Security
Security audit of the own HTTP/1 code (run-2, 2026-10-09)
Scoped standard run of the security-audit workflow over the new code: crates/scalws-h1,
crates/scalws-http/src/h1server.rs (+ conn.rs dispatch), crates/scalws-core/src/upgrade.rs
and the experimental upstream client (crates/scalws-proxy/src/{own_client.rs,h1.rs,upgrade.rs}).
Audited commit d4e3f89; full artifacts in audits/run-2/ (git-ignored): REPORT.md,
FINDINGS-DETAIL.md, NEEDS-VALIDATION.md, findings.json, coverage-ledger.json.
Process: 4 reconnaissance agents, 9 hunters in two waves (the wave-1 coverage critic added two units), 3 coverage critics (the last two found no gaps), an independent verifier per candidate and a second independent verifier per final record — 26 agents. Every local check ran in the dev-host sandbox (isolated namespaces, no network, uid nobody, read-only toolchain, resource limits); evidence was promoted with the no-follow tool.
Findings and fixes
| Finding | Verdict | Fix |
|---|---|---|
| Streamed response kept in memory when the client reads slower than the handler produces (20 MB response → 32 MiB buffer, kept for the next request) | confirmed, medium | the write loop also stops at 256 KiB of buffered bytes; a grown buffer is released after the exchange |
| Handler not cancelled when the client disconnects (slow upstream/PHP work and the connection slot held until the handler’s own timeout) | confirmed, low | while a handler runs and no body is read, the socket is watched; EOF or error cancels the handler (as hyper) |
| Request-body high water counted bytes, not frames (1-byte chunks: 65 536 queued frames) | confirmed, low | reading also pauses at 256 queued frames |
A final 1xx status (e.g. .htaccess Redirect 103) sent unframed with keep-alive, shifting later responses | needs validation (cross-user impact depends on the front proxy) | a final 1xx other than 101 is answered 500 and the connection closes (as hyper) |
100 Continue could be written after or inside the final head | needs validation (no in-tree handler shown to trigger it) | once the final head is queued no 100 is sent; a partly written one is completed first; a still-wanted body closes the connection afterwards |
Regression tests: crates/scalws-http/src/h1server/tests.rs (one per finding). The full
integration suite passes against both servers; workspace clippy is clean.
Hardening notes kept for later
Bare LF accepted in request heads (as hyper); header names with underscores pass through;
only the first Expect is read; Rewind keeps the read buffer for the tunnel’s lifetime;
101 responses are not checked for Upgrade; the PRI sniff polls every 1 ms on a partial
prefix; experimental client: lazy idle expiry with up to 64 × 256 idle connections per
handler, post-write replay of bodyless non-idempotent requests (as the hyper path),
upstream CL lists forwarded verbatim; HTTP/1.1 absolute-form without Host is routed (as the
hyper path).