From 6177c0518e2137cc4f6d039e6ade9b2565db90f6 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 19:16:33 -0400 Subject: [PATCH] fix(egress-proxy-addon): wildcard hosts also match the apex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `*.example.com` now matches `example.com` itself in addition to every subdomain. RFC 6125 TLS-wildcard semantics excluded the apex; an allowlist's natural reading of `*.example.com` is "all of example.com" — and the pipelock mirror already strips `*.example.com` to `example.com`, so without the apex match the two layers disagreed (pipelock allowed the apex, egress-proxy blocked it). Behavior: - `*.example.com` matches `example.com` (apex) - `*.example.com` matches `foo.example.com` (subdomain) - `*.example.com` matches `a.b.example.com` (nested) - `*.example.com` does NOT match `barexample.com` (label boundary required) Test renamed: `test_wildcard_does_not_match_apex` → `test_wildcard_matches_apex`. 395 tests pass. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/egress_proxy_addon_core.py | 19 ++++++++++++------- tests/unit/test_egress_proxy_addon_core.py | 8 ++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/claude_bottle/egress_proxy_addon_core.py b/claude_bottle/egress_proxy_addon_core.py index e77cbc9..967ea06 100644 --- a/claude_bottle/egress_proxy_addon_core.py +++ b/claude_bottle/egress_proxy_addon_core.py @@ -174,10 +174,15 @@ def match_route( Match precedence: 1. Exact (case-insensitive) match on the literal hostname. 2. Wildcard match: a route whose host starts with `*.` is a - suffix pattern. `*.example.com` matches any host that - ends with `.example.com` (so `foo.example.com` and - `a.b.example.com`, but NOT the apex `example.com` or - `barexample.com`). + suffix pattern that covers the apex AND every subdomain. + `*.example.com` matches `example.com`, `foo.example.com`, + and `a.b.example.com`, but NOT `barexample.com` (the + label boundary `.` is required when matching a + subdomain). This is intentionally more permissive than + RFC 6125 TLS-wildcard semantics — an allowlist's natural + reading of `*.example.com` is "all of example.com", + apex included, and matches what the pipelock mirror does + (strips `*.example.com` → `example.com`). Exact match wins over wildcard so an operator can declare a specific route on top of a broader wildcard (e.g. a @@ -189,12 +194,12 @@ def match_route( host = r.host.lower() if not host.startswith("*.") and host == target: return r - # Pass 2: wildcard suffix match (`*.foo.com` → `.foo.com`). + # Pass 2: wildcard match — apex + every subdomain. for r in routes: host = r.host.lower() if host.startswith("*."): - suffix = host[1:] # keeps the leading `.` - if target.endswith(suffix) and target != suffix[1:]: + suffix = host[2:] # strip the `*.` + if target == suffix or target.endswith("." + suffix): return r return None diff --git a/tests/unit/test_egress_proxy_addon_core.py b/tests/unit/test_egress_proxy_addon_core.py index ba89fce..cba1edd 100644 --- a/tests/unit/test_egress_proxy_addon_core.py +++ b/tests/unit/test_egress_proxy_addon_core.py @@ -162,9 +162,13 @@ class TestMatchRouteWildcards(unittest.TestCase): routes = (Route(host="*.example.com"),) self.assertIsNotNone(match_route(routes, "a.b.example.com")) - def test_wildcard_does_not_match_apex(self): + def test_wildcard_matches_apex(self): + # Allowlist semantics: `*.example.com` covers + # `example.com` itself + every subdomain. Matches what + # the pipelock mirror does (strips `*.example.com` → + # `example.com`) so the two layers agree. routes = (Route(host="*.example.com"),) - self.assertIsNone(match_route(routes, "example.com")) + self.assertIsNotNone(match_route(routes, "example.com")) def test_wildcard_does_not_match_overlapping_suffix(self): # `*.example.com` shouldn't match `barexample.com` — the