Skip to content

fix(api): refuse an IP-literal public_origin under a declared TLS posture (ASVS 3.4.1) - #51

Merged
wshallwshall merged 1 commit into
mainfrom
claude/asvs-3-4-1-hsts-ip-literal
Jul 29, 2026
Merged

fix(api): refuse an IP-literal public_origin under a declared TLS posture (ASVS 3.4.1)#51
wshallwshall merged 1 commit into
mainfrom
claude/asvs-3-4-1-hsts-ip-literal

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

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: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 repo has already had to correct.

Where the check lives, and why

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,
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:

  • 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.

Header content also left alone: already max-age=31536000; includeSubDomains, with no preload
an internal hospital hostname must never enter the preload list.

Why the cell stays Partial

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. That is the probe an assessor
actually runs, and it cannot be committed from this repository.

Verification

9585 passed. Two failures, both environmental and reproducing without this change: no .venv in this
worktree (stale 0.3.0 install vs 0.3.2 source), and main's #43 moved the gate source while the
installed user-scope snapshot is stale (install-gate.ps1 re-run).

🤖 Generated with Claude Code

…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.
@wshallwshall
wshallwshall enabled auto-merge (squash) July 29, 2026 19:05
@wshallwshall
wshallwshall merged commit 78c8509 into main Jul 29, 2026
33 checks passed
@wshallwshall
wshallwshall deleted the claude/asvs-3-4-1-hsts-ip-literal branch July 29, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant