Round 2 candidate 17b6d7f6 on 17ea0dbc. 22 of 23 mutants killed, joined-value
equivalent; probes cover repeated Host, proxy Host, HEAD and absolute-form.
r1/ holds the round 1 findings sent to Dewey in place of a verdict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
5.0 KiB
Row 74, WebUI preview behind a sign-in proxy, round 2 review (Darkwing)
Issue #1568, request comment 27484, packet agents/dewey/work/queue-74/
(commit fce04c15). Brief: docs/plans/2026-10-11_webui-preview.md.
Base 17ea0dbc. Candidate manifest sha256
17b6d7f62d106a23d99441eefa46e34ab2e9bd1442a1d02e219f126a82a8668d, 4
files, matching the request.
Verdict: approve. Round 2 closes the two holes from round 1. A repeated Host gets 400 before anything reads the Host, and HEADER with any Host but NAME gets 403, so a proxy that rewrites Host fails on its first request instead of skipping the user check. The HEAD gap is tested now. The checks Sage asked me to press still hold: 401 with nothing read, every write refused, the loopback path unchanged with no flags. One README wording nit, below; nothing blocks.
I sent round 1 findings to Dewey instead of a verdict, at Sage's call.
They are in r1/ for the record: 16 mutants, 13 killed, and the
Host-rewrite reproduction (r1/host-rewrite.txt).
Method
- I read the round 2 delta against my round 1 tree: two checks in
serve.mjs, the README changes, the tests.cli.mjsis unchanged from round 1. - A detached worktree at
17ea0dbcwithgit apply --index build.patch, thensha256sum -con the manifest: 4 OK. None of the 4 files changed between17ea0dbcandHEAD(a55915f0) at the time of review. - On the candidate:
preview.test.mjs11 pass, 0 fail;serve.test.mjs4 pass, 0 fail; the webuibus.test.mjs8 pass, 0 fail. - 23 mutants (
r2/mutants.sh): my 16 from round 1 and 7 aimed at the two new checks. r2/probes.sh: the CLI on port 0 with a missing config and a closed board port, driven with curl and a raw socket on loopback.
I didn't run the full suite gate. Dewey's ran green from 23:15 to 23:22Z and Sage's gate rerun covers landing. The files are webui-only.
Node v26.8.1, TMPDIR=~/darkwing-scratch/rv74r2/tmp.
Mutants
22 of 23 fail (r2/mutants.txt). Each kill failed the test meant for it.
| Mutant | Fails |
|---|---|
head-unsigned: HEAD skips the header check |
the sign-in test (was a survivor) |
head-refused: HEAD gets 403 |
the NAME reads test (was a survivor) |
unsigned-write-403: sign-in checked only for GET and HEAD |
the writes test |
origin-skip-named, single-label-host, ip-host, user-space, user-no-limit, origin-header-ok, header-no-limit |
as in round 1 |
empty-value-ok, named-port-check, noflags-readonly, cli-preview-before-check, cli-no-preview-line |
as in round 1 |
joined-value: req.headers for headersDistinct |
survives, equivalent (ID has no comma) |
repeated-host-off |
the repeated-Host test |
repeated-host-preview-only: 400 only in preview mode |
the repeated-Host test |
repeated-host-after-named: the check removed from the top |
the repeated-Host test |
proxy-host-off |
the proxy-Host test, the NAME reads test |
proxy-host-named-too: !named dropped |
6 tests |
proxy-host-loopback-only: only 127.0.0.1 refused |
the proxy-Host test (localhost, [::1]) |
proxy-host-empty-value: an empty HEADER passes |
the proxy-Host test |
Probes on the candidate (r2/probes.txt)
| Request | Answer |
|---|---|
| Host NAME, wrong user or no HEADER | 401 |
Host 127.0.0.1:PORT, localhost:PORT or [::1]:PORT with HEADER, any value, empty included |
403 proxy Host refused |
| POST with a loopback Host and HEADER | 403 proxy Host refused |
| Loopback Host, then Host NAME, no HEADER (Filbert's case) | 400 |
| Host NAME twice, signed in | 400 |
| HEAD for NAME, unsigned / signed | 401 / 200 |
| No Host (HTTP/1.0) | 403 non-local Host refused |
Absolute-form GET http://NAME/... with a loopback Host, no HEADER |
200, the operator path |
| Loopback Host, no HEADER | 200, the operator path |
The absolute-form case is no wider than a plain loopback request: every check reads the Host header, never the request target's authority.
Note
The tunnel sentence. The README says "A tunnel whose local end uses the same port number sends such a Host and reaches the Console unchecked." A tunnel doesn't send a Host; its clients do. With round 2, a client that sends HEADER through the tunnel gets 403. What stays open is anything that can connect to the tunnel's local end and sends a loopback Host with no HEADER, which matters if that end listens beyond loopback. Something like "Anything that can reach a tunnel's local end on the same port number can send a loopback Host without HEADER and read unchecked, so bind that end to loopback" would say it. A wording fix for a later docs row; not blocking.
Dewey's open question
Should the repeated-Host 400 be preview-only? No, both modes is right.
No browser or proxy sends two Host lines, so nothing legitimate changes
without the flags. Node keeps the first one, so with no flags a loopback
Host followed by any other Host passes the loopback check, and the 400
removes that ambiguity for the local board writes too.
repeated-host-preview-only fails the test, so the choice is pinned.