Skip to content

amuleweb hardening: address #869 #870 #871 #872 #873 #874 - #875

Merged
mrjimenez merged 7 commits into
amule-project:masterfrom
got3nks:feat/amuleweb-hardening
Jun 6, 2026
Merged

amuleweb hardening: address #869 #870 #871 #872 #873 #874#875
mrjimenez merged 7 commits into
amule-project:masterfrom
got3nks:feat/amuleweb-hardening

Conversation

@got3nks

@got3nks got3nks commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Address ngosang's six-issue triage of amuleweb's security + memory-safety surface (#869 #870 #871 #872 #873 #874) in one PR, one commit per issue, in dependency-respecting order. Total ~70 LOC across 4 files.

Verified before commit that all six issues filed today by @ngosang are within scope (gh issue list --author ngosang --search "created:>=2026-06-05" returns exactly #869-#874). No additional security issues filed on the repo are open.

Commits

# Issue Severity What it does Files
67279daa3 #874 Low (cleanup) Wires the long-dead SESSION_TIMEOUT_SECS macro to the live 2-hour timeout that the code uses (was a hardcoded 7200 next to an unused 300). Keeps the live behaviour. WebServer.{h,cpp}
d08d14f28 #873 Medium (memory) Allocates m_dwBufSize + 1 bytes for the read buffer so the unconditional NUL terminator at the end of OnReceive() never falls past the allocation. The narrow trigger window — first Read() exactly fills the buffer AND LastError() is set, skipping the grow loop — is reachable from network input. WebSocket.cpp
503069128 #869 High (XSS) Whitelists the sort query parameter against the column keys my_cmp() actually understands (size / name / sources); falls through to empty otherwise. Defence-in-depth htmlspecialchars for the surviving value. Only direct-echo XSS site in the templates. amuleweb-main-search.php
a6b5f1fec #870 High (auth) Replaces the rand()-into-int session ID generator with a 64-bit token sourced from CryptoPP::AutoSeededRandomPool (same CSPRNG the EC stack uses for DH key agreement). Type ripples from int to uint64_t through ThreadData::SessionID, the m_sessions map, the cookie format, and the cookie parse. Pool reused across calls so OS-RNG seeding cost is paid once per process. WebServer.{h,cpp}, WebSocket.{h,cpp}
bb8cca0af #871 Medium Adds HttpOnly; SameSite=Strict to the Set-Cookie line. Secure deliberately not set — amuleweb has no native TLS and no signal about a fronting proxy; setting it unconditionally would lock out direct-HTTP users. Wiring it to a preference is a clean follow-up. WebSocket.cpp
6908ee9f2 #872 Medium Stashes a CParsedUrl of the pre-POST-body-merge URL in ThreadData::getOnlyParsedURL; the login handler refuses to consume pass when it's reachable via that pre-merge URL. Closes both the GET-with-pass click vector and the POST-with-pass-in-action-URL form vector while preserving #724's POST-on-query-URL fix. WebServer.{h,cpp}, WebSocket.{h,cpp}
807456035 #869 follow-up Rewrites the #869 whitelist to use only PHP constructs that amuleweb's bundled interpreter supports. The original commit used [...] short array syntax + in_array() + htmlspecialchars(), none of which are registered native functions in src/webserver/src/php_core_lib.cpp; caught during test-plan execution on the Ubuntu VM by an amuleweb crash inside php_execute. New impl is a plain == chain over the three whitelisted values; no escaping needed because non-whitelist values aren't echoed. amuleweb-main-search.php

Built clean against amuleweb target on macOS at every commit (cmake --build build --target amuleweb after each edit). No new warnings introduced.

Test plan

Manual smoke testing per fix, plus a regression pass across the parts of amuleweb that don't touch the modified surface but share request-parsing infrastructure. Executed on the Ubuntu ARM64 VM (wxBase 3.2.9 / Boost 1.90, -DCMAKE_BUILD_TYPE=Debug) with amuled + amuleweb on the same host, fresh ~/.aMule/ (no carried-over session state).

#874SESSION_TIMEOUT_SECS wiring

  • Temporarily edit SESSION_TIMEOUT_SECS to 60, rebuild, log in, wait 65 s — verify the same expiry behaviour fires on the shortened timeout. Revert before push. — Confirmed: same session cookie returned the login form (response body contains name="pass") after the 65 s wait, while an immediate refresh before the wait returned the index. Reverted to 7200 and rebuilt before resuming the rest of the test plan.
  • grep proves 7200 no longer appears in src/webserver/src/WebServer.cpp and SESSION_TIMEOUT_SECS is referenced from exactly one call site.

#873 — Off-by-one heap write in OnReceive

  • Rebuild amuleweb with -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" -DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address in a dedicated build-asan tree. Send a normal request (curl http://localhost:4711/). ASAN clean. — Verified: HTTP 200, no ASAN report.
  • With ASAN still on, send a request whose URL pushes against the 4096-byte initial buffer (a 4000-char ?q=... query). Verify ASAN does not flag a heap-buffer-overflow at the m_pBuf[m_dwRecv] = '\0' site. — Verified: HTTP 200, no ASAN report.
  • Send a request large enough to force the grow path (8 KB+ headers, e.g. inflated Cookie: line). Verify the grown buffer's terminator slot is also within the allocation (m_dwBufSize+1 allocation check passes). — Verified with 5 KB and 8 KB cookies; HTTP 200, amuleweb alive (also re-verified under ASAN).
  • Code review: new char[X] sites all carry the +1 (ctor + grow path). No third allocation site missed.

#869 — XSS in sort query parameter

  • Open http://localhost:4711/amuleweb-main-search.php?sort=<script>alert('xss')</script> — verify no alert, view-source shows search_sort= followed by empty (the whitelist rejected it). — Verified via curl + grep, payload dropped to empty.
  • Open ?sort="><img src=x onerror=alert(1)> — verify same: no alert, no attribute breakout. — Verified via curl + grep, payload dropped to empty.
  • Open ?sort=size, ?sort=name, ?sort=sources — verify each renders the link as search_sort=size (etc.) and clicking it sorts the search results by that column (existing behaviour). — All three round-trip correctly.
  • Open without any ?sort= — verify the "use the session's last sort" branch still works (no PHP warning, no XSS). — search_sort= empty, amuleweb alive.
  • grep src/webserver/default/ for any other echo($HTTP_GET_VARS pattern — verify the audit's "this is the only one" claim still holds after the fix.
  • Test-plan execution caught the PHP-builtin incompatibility described above; fixed in 807456035, then re-verified all five scenarios above against the patched amuleweb.

#870 — CSPRNG session IDs

  • Log in to amuleweb, open DevTools → Application → Cookies. Verify amuleweb_session_id is a large value (e.g. a 17-19-digit decimal, ≥ 10^16) — not the small int range rand() produced. — Verified via curl -i: cookie value e.g. 13779537612446685881 (20 digits) and 3358389562839079814 (19 digits).
  • Log out, log back in. Verify the new cookie value is completely different (not a small delta from the previous one). — Confirmed: consecutive draws produce completely unrelated values.
  • Open three browsers / private windows, log in three times. Verify all three IDs are wildly different, none collide. — Sampled 5 fresh draws via curl: 5217712917746154254, 905360975747546453, 1142240790361661942, 2867645926327101357, 182291738831146652. No shared prefixes, no monotonic pattern.
  • Restart amuleweb. Verify previously issued cookies don't authenticate (the in-memory m_sessions map is empty after restart — same behaviour as master tip, just being explicit it didn't regress). — Verified: logged in, killed amuleweb, respawned, sent the same cookie → response body contains name="pass" (login form), no auto-auth.
  • Code: pool is a function-local static, so it's seeded once per amuleweb process, not per request.

#871HttpOnly + SameSite=Strict on the session cookie

  • DevTools → Application → Cookies after login: verify the HttpOnly flag column is ticked and SameSite reads Strict. — Confirmed via curl -i raw response: every Set-Cookie carries amuleweb_session_id=<NN>; HttpOnly; SameSite=Strict. Also confirmed manually in the browser cookie inspector.
  • In the browser console at amuleweb: document.cookie does not include amuleweb_session_idHttpOnly confirmed. — Verified in browser.
  • Cross-site SameSite=Strict check: served a CSRF test page from http://localhost:8001/csrf-mac.html on the Mac (distinct hostname from amuleweb's 192.168.1.139:4711), with a logged-in browser session for amuleweb. Both (a) a top-level cross-site link click to amuleweb-main-dload.php and (b) a cross-site form POST to login.php landed on the login form — the existing session cookie was withheld by the browser, exactly as SameSite=Strict mandates. (Side-note: an initial attempt used a second port on the same hostname as the test origin; that's same-site for cookie purposes — SameSite is computed on registrable hostname, not on origin/port — so it didn't exercise the rule. Re-running with distinct hostnames gave the expected result.)
  • Same-origin sanity: navigate around amuleweb normally. Cookie is sent on every request, browsing works as usual. — Verified via curl --cookie-jar round-trip through all 8 main panels.

#872 — Refuse pass via URL query

  • curl 'http://localhost:4711/login.php?pass=<correct-admin-password>' (GET) → verify the response is the login.php form (no session created with logged_in=true). The amuled stderr should show Refusing to read pass from URL query string. — Confirmed: HTTP 200 response is the login form; log line Refusing to read \pass` from URL query stringthenYou did not enter any password. Blank password is not allowed.`
  • curl -X POST 'http://localhost:4711/login.php?pass=<correct-admin-password>' with empty body → same result: login refused, log line fires. — Empty-body case hits the pre-existing Close() at WebSocket.cpp:135 (Content-Length:0 short-circuit unrelated to this PR), so the request is dropped before ProcessURL runs. With a non-empty body (e.g. --data dummy=1), the Refusing to read path fires as expected.
  • Normal POST login via the form (browser): pass in POST body, no ?pass= on the URL → verify login succeeds as before. — Confirmed: Checking passwordPassword ok → redirected to index.
  • POST to a query-string-bearing URL with a non-pass query param (regression test for Can't login in the webui after latest changes #724): verify the merged parsedURL still picks up both the URL query and the body fields. — Verified: POST /login.php?irrelevant=foo with body pass=webtest succeeds end-to-end (cookie issued, GET of amuleweb-main-dload.php with that cookie returns the control panel, not the login form). Demonstrates that the Can't login in the webui after latest changes #724 ?-vs-& merge logic is preserved alongside the new Credentials accepted via GET and merged into the request URL #872 getOnlyParsedURL capture.
  • Log inspection: every successful login through this path produces no "Refusing to read pass" line; every failed login through ?pass= produces exactly one. — Confirmed against amuleweb stdout.

Regression sweep (across all six fixes together)

  • amuleweb full flow: login → search → start a download → check the queue page → check the downloads page → check the shared-files page → check the log page → log out. Verify each page renders and acts identically to master tip. — Loaded all 8 main panels (dload, shared, search, servers, stats, log, kad, prefs); HTTP 200 across the board, body sizes 2.1–3.5 KB, amuleweb alive throughout.
  • amuled / amulegui / amulecmd unaffected: confirm the EC channel still works (these never touched the webserver code path, but cross-check that the build of the EC libraries didn't regress on mulecommon / mulesocket link dependencies). — amuled stayed up and served amuleweb's EC requests for the duration of testing.
  • Existing user sessions: on first start after deploying this version, any user with a pre-existing cookie will be silently logged out (the in-memory m_sessions map is empty on amuleweb restart, same as today — calling this out so it doesn't get reported as a regression).

Refs

Closes #869, #870, #871, #872, #873, #874.

got3nks added 7 commits June 6, 2026 14:11
…t#874)

WebServer.h's `SESSION_TIMEOUT_SECS 300` macro has been defined for
years but never referenced -- CScriptWebServer::CheckLoggedin tests the
idle window against a hardcoded `7200` (2 hours) instead. The macro's
"5 minutes" documentation has been wrong about live behaviour for as
long as it has existed.

Keep the live behaviour (2 hours -- changing it silently would be a
session-lifetime regression for every existing user) and route the
hardcoded `7200` through the macro so the timeout can be edited in one
place.

Closes amule-project#874
…le-project#873)

The OnReceive() loop reads up to `m_dwBufSize` bytes into `m_pBuf` and
unconditionally writes a NUL at `m_pBuf[m_dwRecv]` to terminate the
buffer for the request-parsing strstr() calls below. The grow loop is
gated on three conditions:

    while (m_dwRecv == m_dwBufSize && read != 0 && !LastError())

If a Read() exactly fills the buffer AND LastError() is set
immediately afterwards (e.g. peer sent 4096 bytes then reset the
connection), the loop exits without growing -- but m_dwRecv ==
m_dwBufSize, so the terminator write at line 85 lands one byte past
the allocation. Heap corruption, narrow trigger window, reachable
from network input.

Fix is the smallest possible thing: allocate `m_dwBufSize + 1` in
both the constructor's initial allocation and in the grow path's
`new char[newsize + 1]`. The usable-span tracking via m_dwBufSize is
unchanged (still the value passed to Read()), so the loop's
`m_dwBufSize - m_dwRecv` arithmetic stays correct and the terminator
slot at index m_dwBufSize is always within the allocation.

Closes amule-project#873
…roject#869)

amuleweb-main-search.php:184 echoed the user-supplied `sort` query
parameter directly into an `<a href=...?search_sort=$X>` attribute
with no escaping. An attacker who can get a user to click a crafted
URL or load a malicious form action gets reflected XSS in the
authenticated amuleweb session -- which, given that amuleweb is the
remote-control surface for amuled, means session theft + remote
daemon control as the logged-in admin.

This template is the only direct-echo XSS site in the webserver
templates (`grep -r 'echo(\$HTTP_GET_VARS' src/webserver/`); the
rest of the user-supplied surface either lives in C++ where it
goes through `_SpecialChars()` first, or is consumed only by
amuleweb's PHP-side handlers rather than reflected to the browser.

Whitelist `sort` against the three column keys `my_cmp()` actually
understands (line 234-236 of this same file: "size", "name",
"sources"). Anything else falls through to empty -- which produces
the "use the session's last sort" branch that the rest of the
template already handles for missing-param case. Belt-and-braces:
`htmlspecialchars()` the surviving value with `ENT_QUOTES` in case
the whitelist later grows a key that needs HTML escaping.

No PHP linter available locally; verified by reviewing the diff
against `my_cmp()`'s switch arms and confirming the three valid
values exactly match.

Closes amule-project#869
)

CScriptWebServer::CheckLoggedin was generating session IDs with C
`rand()` into an `int`, looping on collision. `rand()` is not a CSPRNG,
isn't seeded with anything an attacker can't observe, and yields at
most RAND_MAX (~2^31) candidate values -- making session IDs guessable
in modest time without ever stealing the cookie. The pattern is the
classic "fake session-cookie until you hit a live one and inherit
that user's authenticated context" attack on an admin surface.

Replace with `CryptoPP::AutoSeededRandomPool::GenerateBlock` into a
`uint64_t`:

- 64 bits of entropy is enough that brute-force is no longer a
  realistic attack at amuleweb scale (web request rate, server CPU
  cost per attempt). Going wider (128-bit opaque hex) would also
  work but pulls in cookie-format and storage-type changes for
  marginal improvement.
- AutoSeededRandomPool is the same primitive the EC stack already
  uses for DH key agreement (see EncryptedStreamSocket.cpp), so this
  doesn't drag in a new crypto dependency.
- Reuse the pool across calls via a function-local static so the
  OS-RNG seeding cost is paid once per process, not once per login.

Type ripple (int -> uint64_t):

- `ThreadData::SessionID` (WebServer.h)
- `std::map<int, CSession> m_sessions` -> `std::map<uint64_t, CSession>`
- `CWebSocket::SendHttpHeaders` signature + Set-Cookie format
  (`%d` -> `%llu` with explicit unsigned-long-long cast for the
  windows builds where uint64_t != unsigned long long)
- `int sessid = 0; ... sessid = atoi(...)` -> `uint64_t sessid = 0;
  ... sessid = strtoull(..., 10)` in WebSocket cookie-parse

Closes amule-project#870
…-project#871)

CWebSocket::SendHttpHeaders emitted the session cookie with no
security attributes whatsoever:

    Set-Cookie: amuleweb_session_id=<NN>

Two consequences with practical attack value:

- Any cross-origin script that gets loaded into a page the user is
  visiting can read `document.cookie` and exfiltrate the session
  token. Combined with the reflected XSS in amule-project#869 (or any future
  one), this is "click a malicious link -> attacker has your
  amuleweb session". `HttpOnly` removes that read path entirely --
  the cookie still rides on requests the browser sends to amuleweb,
  it just stops being visible to JavaScript running in the page.

- The browser attaches this cookie to any cross-site request hitting
  amuleweb. An attacker page that fires a fetch / form-submit to
  `http://localhost:4711/login.php?...` rides on the victim's
  authenticated session. `SameSite=Strict` tells the browser to
  refuse the cookie attachment for cross-site requests, closing
  the CSRF lane.

`Secure` is NOT set. amuleweb has no native TLS and doesn't know
whether it's behind a TLS-terminating proxy; setting it
unconditionally would lock out every direct-HTTP user (the browser
refuses to send the cookie, login becomes impossible). Wiring it to
a preference (e.g. `[WebServer] CookieSecure=1`) is a clean
follow-up but out of scope for this hardening pass.

Layered on top of amule-project#870's `uint64_t` session ID so the cookie format
string is the only thing that changes here.

Closes amule-project#871
…t#872)

amuleweb's login handler reads the password via
`Data.parsedURL.Param("pass")`. The wrinkle is the POST-body merge in
CWebSocket::OnRequestReceived: for `POST` requests the body is
concatenated onto the URL string and then handed to CParsedUrl, which
makes the parsed-URL map agnostic about which params came from the
URL query vs which came from the body (the comment at WebSocket.cpp:
197-208 calls out the deliberate behaviour, and links back to amule-project#724
where the fix was originally introduced).

The side-effect: a `pass` query parameter in the original URL gets
into the parsed-URL map and is accepted as the login password. Two
concrete attacks that uses:

- GET-with-pass click: attacker sends the victim
  `https://amule-host/login.php?pass=XYZ`. Browser sends a GET, the
  URL query is the only source, `pass=XYZ` reaches the login handler.

- POST-with-pass-in-URL form: attacker hosts a page with
  `<form action="https://amule-host/login.php?pass=XYZ" method="POST">`.
  Victim clicks, browser POSTs, the POST body might be empty but
  the URL still carries `?pass=XYZ` -> same outcome.

Passwords in URLs also leak into proxy logs, browser history, and
`Referer` headers, so this isn't only an attacker-controlled-link
problem.

Fix is the Option B from the design discussion: capture the
*pre-merge* URL on the wire and stash it alongside the merged one in
`ThreadData::getOnlyParsedURL`. In the login handler, before reading
`Data.parsedURL.Param("pass")`, check whether `pass` is reachable
from the pre-merge URL -- if it is, refuse to consume it. The merged
map stays the source of truth for every non-credential parameter, so
the amule-project#724 POST-on-query-URL fix isn't disturbed.

Side-effects across the rest of the code:

- `ThreadData` gains a `CParsedUrl getOnlyParsedURL` field; the one
  brace-init call site in WebSocket.cpp:231 grows from 4 fields to
  5. No allocation cost on the hot path: CParsedUrl is just two
  wxStrings + a map<wxString,wxString>, identical to the existing
  parsedURL field.
- For GET requests `getOnlyParsedURL` parses the same string as
  `parsedURL`, so the only `pass` check fires identically in either
  case. For POST requests with `pass` only in the body, the pre-merge
  URL has no `pass` entry and the password is read normally -- the
  intended login flow keeps working.

Closes amule-project#872
Caught while executing PR amule-project#875's test plan on the Ubuntu VM. My
original amule-project#869 fix at 5030691 used three constructs the bundled
amuleweb PHP interpreter doesn't actually support:

- `[...]` short array syntax (the interpreter only accepts
  `array(...)`).
- `in_array(...)` -- not a registered native function. Grep of
  `src/webserver/src/php_core_lib.cpp` shows the entire builtin set
  is `var_dump / strlen / count / isset / usort / split` plus the
  gettext family. Calling an unregistered function path-crashes
  `php_execute` at php_syntree.cpp:1819 on the first request that
  reaches the search template.
- `htmlspecialchars(...)` -- same: not registered, same crash.

Reproduction: log in, render any template -- amuleweb segfaults
inside php_execute. Backtrace points at the search template's
PHP block. Captured in the test session under PR amule-project#875.

Rewrite the whitelist as a plain `==` chain. The three valid sort
keys (`size`, `name`, `sources`) are static alphanumeric column
names; no escaping is needed because non-whitelist values simply
don't get echoed at all -- the only escape-sensitive scenario would
have been a whitelisted value containing HTML-special characters,
which is impossible by construction. Net effect on the XSS surface
is identical: attacker-supplied `sort` payloads never reach the
rendered HTML.

Re-verified after this change: the crash is gone, sort=size /
sort=name / sort=sources all set the correct anchor href, payload
sort values fall through to empty.

Refs amule-project#869 amule-project#875
@danim7

danim7 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Replaces the rand()-into-int session ID generator with a 64-bit token sourced from CryptoPP::AutoSeededRandomPool (same CSPRNG the EC stack uses for DH key agreement)

Do you mean the External Connections (EC) protocol uses Diffie-Hellman key agreement? Last time I checked, the packets were sent as plaintext and could be easily read with wireshark, but maybe I'm missing something.

@got3nks

got3nks commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

You're right — sloppy phrasing on my side. EC is plaintext (MD5 challenge-response auth, no DH); AutoSeededRandomPool is used at EncryptedStreamSocket.cpp:404 for the ed2k client-protocol obfuscation handshake (m_cryptDHA.Randomize(...) picks the DH secret exponent for the per-connection key derivation), not the EC stack. The CSPRNG choice is still well-grounded — it's the project's existing pool at RandomFunctions.cpp:31 — but my justification pointed at the wrong subsystem. Sorry for the confusion.

@danim7

danim7 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Thanks, i just wanted to make sure i was not missing that feature.

got3nks added a commit to got3nks/amule that referenced this pull request Jun 7, 2026
…ule-project#912)

Extends existing categories (preferring extensions over new lines):
- Performance/Upload: amule-project#898 SlotAllocation default raised.
- Networking & Discovery: wire-parser hardening list extended with
  amule-project#879/amule-project#882/amule-project#890/amule-project#886; new amuleweb security hardening bullet
  consolidating ngosang's amule-project#869-amule-project#874 triage (all landed in amule-project#875);
  amulegui list extended with amule-project#857; shared-folder watcher extended
  with amule-project#858.
- Packaging: Windows installer i18n line extended with amule-project#899.
- Internals & Refactoring: new docs-polish + code-quality bullets
  covering amule-project#851/amule-project#855/amule-project#862/amule-project#888/amule-project#900/amule-project#866/amule-project#867/amule-project#895 and amule-project#909/amule-project#910/amule-project#912.
- Translations: new pre-release final-wave bullet covering amule-project#847/amule-project#856/
  amule-project#891/amule-project#908/amule-project#860/amule-project#904/amule-project#859/amule-project#863/amule-project#861/amule-project#880/amule-project#911/amule-project#901/amule-project#902/amule-project#889/amule-project#868/amule-project#853.
- Bug Fixes & Stability: amule-project#850/amule-project#854/amule-project#878/amule-project#906.
- CI: ccache wiring (amule-project#892, amule-project#903) + CodeQL binutils-dev (amule-project#907).

Contributors footer gains mifritscher and nguyenhoangminhhieu2004-gif
(both first-time contributors).

PR index extended through amule-project#912.
mrjimenez pushed a commit that referenced this pull request Jun 8, 2026
Extends existing categories (preferring extensions over new lines):
- Performance/Upload: #898 SlotAllocation default raised.
- Networking & Discovery: wire-parser hardening list extended with
  #879/#882/#890/#886; new amuleweb security hardening bullet
  consolidating ngosang's #869-#874 triage (all landed in #875);
  amulegui list extended with #857; shared-folder watcher extended
  with #858.
- Packaging: Windows installer i18n line extended with #899.
- Internals & Refactoring: new docs-polish + code-quality bullets
  covering #851/#855/#862/#888/#900/#866/#867/#895 and #909/#910/#912.
- Translations: new pre-release final-wave bullet covering #847/#856/
  #891/#908/#860/#904/#859/#863/#861/#880/#911/#901/#902/#889/#868/#853.
- Bug Fixes & Stability: #850/#854/#878/#906.
- CI: ccache wiring (#892, #903) + CodeQL binutils-dev (#907).

Contributors footer gains mifritscher and nguyenhoangminhhieu2004-gif
(both first-time contributors).

PR index extended through #912.
@got3nks
got3nks deleted the feat/amuleweb-hardening branch June 8, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reflected XSS via unescaped sort parameter in search template

3 participants