fix(webconsole): report the browser-hardening opt-out at start (BACKLOG #1118) - #782
Conversation
…OG #1118) ADR 0172 dissolved this item's premise. The engine always serves TLS, so effective_https holds on the shipped default and both cookies already resolve to their __Host- twins with no cookie-code change. Measured, with controls, and recorded in the item. What is left is the one residual the item raised but did not grade: MEFOR_WEBCONSOLE_DISABLE_BROWSER_HARDENING reverted the cookies to their bare names and nothing at start said so. The serve ladder now names the variable, the two unprefixed names, and the fact that Secure is not downgraded. This is a report, not a control. Nothing here re-scores the cell on a warning. Also pins the trap the research warned about: a minted certificate must not make exposure_protected true, or a self-signed placeholder would satisfy the off-loopback /ui gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed. The finding is that there was nothing to build, and proving that is the deliverable. I flagged in the brief that ADR 0172 might already have pulled this item's lever, because the same thing had just dissolved a sibling item's research contest. This PR settles it with a positive control on every reading, which is what makes it a measurement rather than an assertion: the cookie names resolve to their It did not manufacture cookie work to have something to ship. That is the outcome I asked for and the easy one to get wrong. The security trap I warned about turns out to be already avoided, and pinning it is the right response. What it built is the honest residual, the one the cell raises but does not grade: Exporting the predicate through Collision avoided cleanly: it did not touch The glyph check with a positive control of 889 hits is the correct instrument -- a pattern that finds nothing anywhere is indistinguishable from a clean tree. Reviewer independence, stated rather than assumed. I wrote this Builder's brief, so I am not independent of its scope; I am reviewing the diff, which I did not write. The owner has declined a separate Reviewer seat. Auto-merge is not armed. |
The premise measurement came back dead, and that is the headline
BACKLOG #1118 asked how to reach ASVS 3.3.3 without breaking a cleartext loopback login. ADR 0172 landed since that research and removed the cleartext loopback bind, so there is nothing left to trade off. The engine always serves TLS,
effective_httpsholds on the shipped default, and both cookies already resolve to their__Host-twins. No cookie code changed in this PR.I measured before building anything, with a positive control on every reading.
ensure_api_tls_material(ApiSettings())tls_terminated_upstreamtopology returnsNone, so the proxy's own hop is not brokeneffective_httpsover the served schemeTrueat schemehttpsFalseat schemehttp, the pre-0172 defaultsession_cookie_name__Host-mf_sessionmf_sessionoverhttpoidc_flow_cookie_name__Host-mf_oidc_flowmf_oidc_flowoverhttpApiSettings().tls_enabled/.exposure_protectedFalse/FalseTrueThe last row is the security trap the brief named, and the shipped code already avoids it.
__main__.pycopies the minted paths into a localmodel_copythat uvicorn reads and no gate does, so a self-signed placeholder never satisfies the off-loopback/uiexposure gate. The "generated-and-serving third state" the research asked for exists; it is just spelled as a local copy rather than a new field. I pinned it with a test so a later refactor cannot simplify it back intosettings.api.This covers every startable topology, not only the loopback default. The one posture still reaching the app over cleartext is a declared upstream terminator, and settings validation forces
trusted_proxiesalongside it, soexposure_protectedis true there andeffective_httpsholds anyway. There is no switch that returns the API socket to cleartext.Import-path verification
Run with the primary interpreter and this worktree as cwd:
messagefoundryresolves inside the worktree too. Every reading above is against this branch's tree, not the primary checkout's.What I built instead, and what I deliberately did not
I did not build the cookie work. There is none left. I built the one residual the item raised but does not grade.
MEFOR_WEBCONSOLE_DISABLE_BROWSER_HARDENINGreverts both cookies to their bare names, and nothing at start said so. The predicate was read only from_auth.pyand_security.py, both per-request. An operator who set it, or inherited it from a service environment, got a quietly weaker console with no signal anywhere. The serve ladder now names the variable, the two unprefixed names it reverts to, and the fact that Secure is not downgraded.This is a report, not a control. The 2026-08-17 ruling that a warning earns nothing by itself still holds, and nothing here re-scores the cell on it. It matters more since ADR 0172 than before, for one plain reason: this env is now the only remaining way a deployment would lose the browser-enforced host binding. Before 0172 a cleartext bind lost it too, so this was one signal among several.
Severity, in the conditional the repo requires: a site that set this variable would lose the
__Host-binding on both cookies, and until this change nothing would have told them.Files
messagefoundry/__main__.py/uiladdermessagefoundry_webconsole/__init__.py_authtests/test_cli.pytests/test_api_tls.pydocs/BACKLOG.mdNegative control on the new advisory test: with
__main__.pyand the console__init__.pystashed,test_browser_hardening_opt_out_is_reported_at_startfails withassert 'MEFOR_WEBCONSOLE_DISABLE_BROWSER_HARDENING is set' in ''. The absence test passes either way, which is correct for a control.Collision with PR 756
PR 756 (
claude/backlog-1117-cookie-clear-symmetry) is open inmessagefoundry_webconsole/_auth.py, on the cookie clear path, immediately beside the resolution sites this item anchors on. I did not touch_auth.pyat all, and I did not refactor the resolution helpers or the shared predicates.The one shared file is
docs/BACKLOG.md. PR 756's hunk is at roughly line 7546 (item #1117); mine are at 410 and 7692 onward (item #1118). Separate hunks, about 150 lines apart, so they should merge clean. If a reviewer sees a conflict there, mine is the additive block at the end of #1118.I also recorded the anchor drift in the item and told future readers to cite the two sites by function name rather than line number, precisely because 756 is about to move those lines.
Checks run and skipped
ruff check(messagefoundry, messagefoundry_webconsole, tests)ruff format --checktests/test_cli.pymypystrict (300 source files)pytest tests/test_api_tls.py tests/test_cli.py tests/test_webconsole_{absent,mount,seam_snapshot}.pypytest packaging/messagefoundry-webconsole/tests/test_ui_hardening.pypytest tests/test_tooling_partition.py tests/test_phi_logging_inventory.pypytest tests/ -k backlogscripts/docs/backlog_status_check.pywindows-service-smoke, load legs)Glyph scan on the 162 added lines: zero hits, against a positive control of 889 hits for the same pattern over the whole of
docs/BACKLOG.md.What is left, and it is not a build
The closing act on #1118 is a scorecard re-score, and that record lives in the separate vault repository. I did not edit, commit or push anything there, and no cell content, coverage or gap appears in this PR. Whoever holds that record can now read a measurement instead of a plan.
Three residual disclosures from the research are unchanged by this pass and still belong on the cell:
http://127.0.0.1was already a secure context.__Host-binds to the host, so a co-resident127.0.0.1:*origin can still shadow by port.Open question for the Console, since a Builder cannot ask and wait: the item is still banner-open with
Closing-act: scorecard-rescore, so I left its banner alone. If the fleet wants #1118 closed on the strength of this measurement, that needs the vault act first, and it needs a seat that may write there.🤖 Generated with Claude Code