fix(webconsole): the browser-hardening opt-out drops __Host- only, so __Secure- is the fallback (BACKLOG #1117) - #934
Open
wshallwshall wants to merge 1 commit into
Open
Conversation
… __Secure- is the fallback (BACKLOG #1117) Owner ruling 2026-09-05. It had never been written down whether the org opt-out means "no __Host-" or "no cookie-name prefix at all", and the code took the second reading, so both /ui cookies went bare on every opt-out posture and ASVS 3.3.1's second limb failed there. session_cookie_name and oidc_flow_cookie_name now answer in three branches: the transport decides first, and only then does the hatch choose between __Host- and __Secure-. The set sites reach both names through those same resolvers, so PR 868's single-resolver guarantee is unchanged. The fallback is taken only where Secure is genuinely set, asserted rather than assumed: a browser drops a __Secure- cookie without Secure exactly as it drops a __Host- one. Name and Secure key on the same effective_https predicate through separate calls, because #1117 forbids inferring either conjunct from the other. The serve-time opt-out report named the bare cookies; it now names the __Secure- twins and why Secure is what keeps them writable. New wire-level tests serve /ui under a real uvicorn on an ephemeral loopback port through the shipped TLS wiring and read the response's own Set-Cookie. Both opt-out arms were proved red first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the owner ruling of 2026-09-05 on BACKLOG #1117 (ASVS 3.3.1), and records it in the item.
PR 908 settled that item's lens contest by measurement and left exactly one thing open: nobody had written down whether the org browser-hardening opt-out means "no
__Host-" or "no cookie-name prefix at all". Its builder correctly declined to answer that by building. The owner has ruled: it drops__Host-ONLY.__Secure-is therefore the pinned verb's required fallback, and this PR is the code half.What changed
session_cookie_name,oidc_flow_cookie_name(messagefoundry_webconsole/_auth.py)__Host-or__Secure-mf_session/mf_oidc_flow, with Secure set__Secure-mf_session/__Secure-mf_oidc_flowserve_uiopt-out report (messagefoundry/__main__.py)mf_session / mf_oidc_flow__Secure-twins, and says Secure is what keeps them writableAnchors are cited by symbol, not by line: #1117 records that these have already drifted twice.
Three things the item forbids, and what this does instead
exposure_protected. Untouched. The same predicate carries 3.3.3, and widening it would make it lie.effective_https.effective_httpscall. They agree because they read the same predicate, not because either derives from the other.The fallback is only correct where Secure is genuinely present -- a browser drops
__Secure-without Secure exactly as it drops__Host-. PR 908 measured Secure present on both opt-out arms; that was re-verified here rather than relied on, and every new test asserts the Secure attribute in the same row as the name, so the two cannot be graded apart. The cleartext arm keeps the bare name for that reason and is pinned as the negative control.PR 868 made
session_cookie_namethe single resolver every set, clear and read site threads through. No second copy of its expression was introduced: the set sites still call the resolvers, which is where the new branch lives.Tests, and each was proved RED first
packaging/messagefoundry-webconsole/tests/test_ui_cookie_prefix_wire.py(new). Extends PR 908's pattern rather than asserting a string: a real uvicorn on an ephemeral loopback port, wired through the shipped TLS chain (ensure_api_tls_material->build_api_ssl_context-> uvicorn'sssl_context_factory, the same three callsservemakes), a real login POST, reading the response's ownSet-Cookie. The scheme is then a fact about the socket, not a literal handed to an ASGI transport.mf_session, Secure set__Secure-mf_sessionexposure_protectedmf_session, Secure set__Secure-mf_session__Host-mf_sessionmf_session, no SecureMeasured red before the code moved:
AssertionError: assert 'mf_session' == '__Secure-mf_session'on both opt-out arms, with the two controls green in the same run -- so the file was not vacuously red.test_ui_hardening.py.test_the_opt_out_drops_host_only_and_keeps_the_secure_prefixis new and parametrised over three postures, covering the flow cookie (which the wire test's login leg does not emit) and asserting the resolver never returns__Host-under the opt-out. Two existing tests moved from the bare name to__Secure-mf_session; both were red before the change and green after.tests/test_cli.py. The opt-out report assertion now requires the__Secure-names. A report naming a cookie the browser is not holding would send an operator hunting the wrong thing.The re-score trigger PR 908 flagged: already restored, and not duplicated
PR 908's added text carries "Restore that trigger." with its reasoning, so this pass does not repeat it. What it does add is upstream of that: the item's "Scope corrections recorded here so they are not re-derived" paragraph still asserted the ground for the 2026-08-20 strike (
__Secure-requires Secure exactly as__Host-does, so no reachable state has one available and not the other) as current fact. That is now marked struck in place, left standing rather than deleted because it was acted on -- a reader who meets the strike needs to find its reason and find it refuted.The two now-unreachable cleartext tests: ANNOTATED, not changed
PR 908 flagged
test_http_cookie_is_byte_identicalandtest_loopback_http_engages_headers_but_keeps_plain_cookieas making a reader conclude the shipped default is cleartext. Annotated, not fixed. Each docstring now says nomessagefoundry serveposture has reached that branch since ADR 0172, and points at the wire-level file for what actually ships. They are kept rather than deleted because they are the control that stops an unconditional rename -- deleting them would remove the only assertion that a genuinely cleartext origin must keep the bare name.Merge note: expect a conflict with PR 908
PR 908 is open, docs-only, and inserts at the end of #1117's block; so does this PR's new entry. Both are pure insertions before
## 1118., so git will very likely conflict there. The resolution is to keep both, PR 908's first. This PR's in-place edit to the scope-corrections paragraph does not overlap PR 908's diff. Land 908 first if you have the choice.Checks
Run from this worktree with the primary checkout's interpreter (no
.venvhere; import source confirmed to resolve to this worktree, printed besidesys.executable, not the primary).ruff format --check .-- 1257 files already formattedruff checkon every changed file -- passedmypy messagefoundry messagefoundry_webconsole(strict) -- Success, 301 source files. CI type-checks those two trees only, so the new test file is outside its scope.packaging/messagefoundry-webconsole/tests-- 429 passed, 3 skippedtests/test_cli.py-- 98 passedtests/test_api_tls.py tests/test_security_static.py tests/test_threat_model_doc_drift.py-- 148 passed, 89 skippedtests/test_backlog_status_check.py test_backlog_citation_check.py test_doc_guards_lane.py test_cp1252_console_safety.py test_asvs_rescore_handoff.py test_ledger_check.py test_link_resolution.py-- 192 passedbacklog_status_check.py --quiet,ledger_check.py docs/BACKLOG.md,dangling_citation_check.py,asvs_tally_lint.py,verdict_divergence_check.py,control_char_check.py,scan_forbidden.py-- all exit 0Skipped: the rest of the engine suite, and every hosted-runner-only leg (
windows-service-smokeamong them). Nothing in this diff touches the service, the store or a transport./simplifywas run on the changed code -- it folded the negative-control arm back onto the shared server fixture and removed a**kwargsconstruction plus its type ignore.Not done, deliberately