fix(api): refuse an IP-literal public_origin under a declared TLS posture (ASVS 3.4.1) - #51
Merged
Merged
Conversation
…ture (ASVS 3.4.1) Reproduced before fixing: https://10.20.30.40:8443 and https://[2001:db8::1] were both ACCEPTED with tls_terminated_upstream declared. RFC 6797 8.1.1 says a UA "MUST NOT note" an IP-literal host as a Known HSTS Host, so the Strict-Transport-Security header the engine emits for such an origin is required to be DISCARDED by every conforming browser. The console therefore had no HTTPS-downgrade protection while the header, the config and the scorecard all reported the control as present. That is worse than an absent control, because nothing prompts anyone to look -- the same shape as the threat-model absence claim and the un-fingerprinted rotation class this codebase has already had to correct. Checked in the model validator, not the public_origin field validator: the field alone cannot see the posture, and a bare http://127.0.0.1 origin with no TLS declared is a legitimate dev flow. The refusal is about a header that would be silently discarded, NOT about IP literals as such. Verified boundary: IPv4 and IPv6 literals refused under a declared TLS posture; DNS names accepted; the loopback dev flow untouched. Three mutations red, and the last two matter most because they are the over-broad failure modes a positive control exists to catch: - neutralise the IP-literal detection -> DID NOT RAISE - drop the TLS-posture condition -> the loopback dev case now raises (every developer on 127.0.0.1 locked out) - invert the detection -> DNS names refused instead NOT built, deliberately: a "nothing refuses today" gate. That premise is false and one grep disproves it -- __main__.py:1768-1782 already returns 2 on an http:// origin with any declared TLS posture, so an https public_origin is ALREADY mandatory on the scored posture. Building a gate that already ships is the mirror image of claiming a control that does not; both end in a retraction. This does NOT close 3.4.1. Item (b) -- `always` on the HSTS add_header in the reference proxy blocks, so it survives the 502/504/413/431 responses the proxy generates and the engine's _security_headers middleware never sees -- lives only in the vault-bound OFF-LOOPBACK-DEPLOYMENT.md, and that is the probe an assessor actually runs. The cell stays Partial until the doc half lands. Also left alone on purpose: header content is already max-age=31536000; includeSubDomains, with no `preload` -- an internal hospital hostname must never enter the preload list.
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.
ASVS 3.4.1 — the code half. This does not close the cell (see the bottom), but it removes a
control that was reporting success while doing nothing.
Reproduced before fixing
https://10.20.30.40:8443andhttps://[2001:db8::1]were both accepted withtls_terminated_upstreamdeclared. RFC 6797 §8.1.1 says a UA "MUST NOT note" an IP-literal host asa Known HSTS Host — so the
Strict-Transport-Securityheader the engine emits for such an origin isrequired to be discarded by every conforming browser.
The console therefore had no HTTPS-downgrade protection while the header, the config and the
scorecard all reported the control as present. That is worse than an absent control, because nothing
prompts anyone to look — the same shape as the threat-model absence claim and the un-fingerprinted
rotation class this repo has already had to correct.
Where the check lives, and why
The model validator, not the
public_originfield validator: the field alone cannot see theposture, and a bare
http://127.0.0.1origin with no TLS declared is a legitimate dev flow. Therefusal is about a header that would be silently discarded, not about IP literals as such.
Verified boundary — IPv4 and IPv6 literals refused under a declared TLS posture, DNS names accepted,
loopback dev flow untouched.
Mutations
Three red, and the last two matter most because they are the over-broad failure modes a positive
control exists to catch:
DID NOT RAISE127.0.0.1locked out)
Not built, deliberately
A "nothing refuses today" gate. That premise is false and one grep disproves it:
__main__.py:1768-1782already returns2on anhttp://origin with any declared TLS posture, so anhttps
public_originis already mandatory on the scored posture. Building a gate that alreadyships is the mirror image of claiming a control that does not — both end in a retraction.
Header content also left alone: already
max-age=31536000; includeSubDomains, with nopreload—an internal hospital hostname must never enter the preload list.
Why the cell stays Partial
Item (b) —
alwayson the HSTSadd_headerin the reference proxy blocks, so it survives the502/504/413/431 responses the proxy generates and the engine's
_security_headersmiddleware neversees — lives only in the vault-bound
OFF-LOOPBACK-DEPLOYMENT.md. That is the probe an assessoractually runs, and it cannot be committed from this repository.
Verification
9585 passed. Two failures, both environmental and reproducing without this change: no
.venvin thisworktree (stale
0.3.0install vs0.3.2source), and main's #43 moved the gate source while theinstalled user-scope snapshot is stale (
install-gate.ps1re-run).🤖 Generated with Claude Code