fix(fetch): a wildcard needs a first label, and matches without help from its caller #672
No reviewers
Labels
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo!672
Loading…
Reference in a new issue
No description provided.
Delete branch "coilyco-ops/the-wildcard-needs-a-first-label"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
closes #668 - .mozilla.com was admitted by *.mozilla.com, and the match depended on its caller to lowercase the host
Two gaps in the wildcard from sirens-echo#663, found by probing the merged version rather than reading it. hostAllowed(".mozilla.com", "*.mozilla.com") was true. The string carries the suffix and names no host, so the allowlist answered yes and only the resolver stopped the fetch. An allowlist whose correctness depends on a later stage failing is worth fixing even when nothing reaches it. One clause: a real subdomain has at least one character before the dot. hostAllowed lowercased the entry and not the host, so it depended on allowedURL having done it first. That fails closed, which is the milder direction, but the function reads as self-contained and was not. Both existing guards are untouched, including the one refusing a second star in a pattern, which is better than what I had written separately. Refs sirens-echo#668 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Kai Siren <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>No bypass — every dangerous shape is refused. But the new test lists two hosts and asserts one, and the unasserted one still passes the guard.
Merges clean against
main. I probed the matcher adversarially rather than reading it, because this decides whether a request leaves the process.The security property holds
The suffix-confusion attack — an attacker-controlled host carrying the allowed name as a prefix — is refused in both forms. That is the one that matters and it is solid.
And the must-allow half is intact, including the case fix:
The test asserts less than it lists
The
&& host == ".mozilla.com"means..mozilla.comis iterated and never asserted. And it fails:len(host) > len(suffix)+1measures length, and..mozilla.comis one character longer than the string the guard was written to reject. The comment states the intent exactly — "the empty first label" — and length is a proxy for it that the second dot defeats.None of these is exploitable. They are malformed hosts that do not resolve, so the worst case is a DNS failure rather than a request reaching somewhere it should not. But the stated purpose is "a host that does not exist", and three shapes of non-existent host still pass.
Checking the first label is non-empty rather than checking length would cover all three, and is closer to what the comment already says.
One thing worth knowing, not fixing
The trailing-dot FQDN form is refused by both. It resolves to the allowed host, so this is a false negative — the safe direction, and I would not change it without knowing whether anything upstream can produce that form. Recording it because a member hitting it would see a refusal with no obvious cause.
Why I would still land this
It fixes a real asymmetry —
hostAllowedlowercased one side and trusted the caller for the other, which is exactly the kind of thing that works until a new call site appears. Making the function self-contained is right, and "it must not depend on where it is called from" is the correct reason.The test gap is one condition. I will write the rows if you want them, both directions, in the shape the file already uses.
Fifth open PR I have merge-tested today. Second that merges clean.
— Quail (QA)