fix(link): 3 more codex [P2] findings — per-route caps, bool rules, proxy validation
Round 2 of review on the same branch: - The fail-closed capacity guard was itself a DoS lever. MAX_RATE_KEYS was global, and the limiter runs before auth, so a flood of cheap /v1/repos keys could exhaust the table and 429 never-seen /v1/link clients until live windows expired. Budgets are now per route. - Rule values accepted booleans: bool subclasses int, so isinstance let [5, true] through as a 1-SECOND window (5/hour -> ~5/sec) and false in the limit slot disabled the endpoint. Now `type(x) is int`. - trusted_proxies was unvalidated: a bare string would be iterated character by character, malformed entries only surfaced as a per-request log line, and 0.0.0.0/0 or ::/0 restored "trust XFF from any peer" — the exact hole the setting closes. Now parsed and validated once at startup, wildcards refused, and _ip_in_any takes pre-parsed networks so nothing can degrade to a silent per-request skip. Tests 99 -> 108: cross-route flood isolation, per-route reclamation windows, every bool-in-rule position, bare-string and wildcard proxies, and a v4/v6 mismatch case. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LTARYHX7GPepi3CH3tp5pg
This commit is contained in:
co-authored by
Claude Opus 5
parent
c442721aff
commit
c742ca4798
+91
-10
@@ -544,7 +544,7 @@ class TestRateLimiterUnit(unittest.TestCase):
|
||||
for i in range(500):
|
||||
self.now += 0.001
|
||||
rl.check("/v1/link", f"10.0.{i // 256}.{i % 256}")
|
||||
self.assertLessEqual(len(rl._hits), 50 + 1)
|
||||
self.assertLessEqual(len(rl._hits["/v1/link"]), 50 + 1)
|
||||
|
||||
def test_expired_keys_are_reclaimed(self):
|
||||
rl = granthi_link.RateLimiter(rules={"/v1/link": (5, 60)},
|
||||
@@ -554,7 +554,7 @@ class TestRateLimiterUnit(unittest.TestCase):
|
||||
self.now += 120 # everything expires
|
||||
for i in range(10, 25):
|
||||
rl.check("/v1/link", f"10.0.0.{i}")
|
||||
self.assertLessEqual(len(rl._hits), 11)
|
||||
self.assertLessEqual(len(rl._hits["/v1/link"]), 11)
|
||||
|
||||
def test_concurrent_checks_never_exceed_the_limit(self):
|
||||
"""The lock has to actually hold under threads: 40 racing callers
|
||||
@@ -585,7 +585,9 @@ class _FakeHandler:
|
||||
|
||||
|
||||
class TestClientIp(unittest.TestCase):
|
||||
PROXY = ("10.0.0.0/8",)
|
||||
# Pre-parsed, exactly as LinkService._parse_trusted_proxies hands them
|
||||
# over -- validation happens once at startup, never per request.
|
||||
PROXY = granthi_link.LinkService._parse_trusted_proxies(["10.0.0.0/8"])
|
||||
|
||||
def test_socket_peer_by_default(self):
|
||||
h = _FakeHandler("5.5.5.5", xff="1.2.3.4")
|
||||
@@ -618,10 +620,16 @@ class TestClientIp(unittest.TestCase):
|
||||
self.assertEqual(granthi_link.client_ip(h, True, self.PROXY),
|
||||
"10.0.0.1")
|
||||
|
||||
def test_malformed_trusted_proxy_entry_does_not_grant_trust(self):
|
||||
def test_malformed_trusted_proxy_entry_is_rejected_at_startup(self):
|
||||
"""It never reaches a request: a bad CIDR kills the process rather
|
||||
than degrading to a per-request log line."""
|
||||
with self.assertRaises(SystemExit):
|
||||
granthi_link.LinkService._parse_trusted_proxies(["not-a-cidr"])
|
||||
|
||||
def test_v4_peer_does_not_match_a_v6_trusted_net(self):
|
||||
nets = granthi_link.LinkService._parse_trusted_proxies(["fd00::/8"])
|
||||
h = _FakeHandler("10.0.0.1", xff="9.9.9.9")
|
||||
self.assertEqual(
|
||||
granthi_link.client_ip(h, True, ("not-a-cidr",)), "10.0.0.1")
|
||||
self.assertEqual(granthi_link.client_ip(h, True, nets), "10.0.0.1")
|
||||
|
||||
|
||||
class TestRateLimitConfig(ServiceTestBase):
|
||||
@@ -693,14 +701,14 @@ class TestRateLimitHardening(unittest.TestCase):
|
||||
max_keys=5, clock=lambda: now[0])
|
||||
for i in range(5):
|
||||
self.assertTrue(rl.check("/v1/link", f"10.0.0.{i}")[0])
|
||||
victim_hits = list(rl._hits[("/v1/link", "10.0.0.0")])
|
||||
victim_hits = list(rl._hits["/v1/link"]["10.0.0.0"])
|
||||
for i in range(100, 140): # identity flood
|
||||
allowed, retry = rl.check("/v1/link", f"10.0.1.{i}")
|
||||
self.assertFalse(allowed)
|
||||
self.assertGreater(retry, 0)
|
||||
# the earlier client's window survived the flood untouched
|
||||
self.assertEqual(rl._hits[("/v1/link", "10.0.0.0")], victim_hits)
|
||||
self.assertLessEqual(len(rl._hits), 5)
|
||||
self.assertEqual(rl._hits["/v1/link"]["10.0.0.0"], victim_hits)
|
||||
self.assertLessEqual(len(rl._hits["/v1/link"]), 5)
|
||||
|
||||
def test_capacity_recovers_once_windows_expire(self):
|
||||
now = [1000.0]
|
||||
@@ -723,7 +731,7 @@ class TestRateLimitHardening(unittest.TestCase):
|
||||
t.start()
|
||||
for t in ts:
|
||||
t.join()
|
||||
hits = rl._hits[("/v1/link", "7.7.7.7")]
|
||||
hits = rl._hits["/v1/link"]["7.7.7.7"]
|
||||
self.assertEqual(len(hits), 200)
|
||||
self.assertEqual(hits, sorted(hits), "timestamps out of order")
|
||||
|
||||
@@ -770,3 +778,76 @@ class TestRateLimitConfigTypes(ServiceTestBase):
|
||||
"trusted_proxies": ["10.0.0.0/8", "127.0.0.1/32"]})
|
||||
self.assertTrue(svc.trust_forwarded_for)
|
||||
self.assertEqual(len(svc.trusted_proxies), 2)
|
||||
|
||||
|
||||
class TestRateLimitRound2(unittest.TestCase):
|
||||
"""The three findings from the codex re-review."""
|
||||
|
||||
def test_flooding_one_route_cannot_lock_out_another(self):
|
||||
"""A shared key table turned the fail-closed capacity guard into a
|
||||
cross-route DoS: cheap /v1/repos keys could exhaust it and shut new
|
||||
/v1/link clients out. Budgets are per route."""
|
||||
now = [1000.0]
|
||||
rl = granthi_link.RateLimiter(
|
||||
rules={"/v1/link": (5, 3600), "/v1/repos": (60, 3600)},
|
||||
max_keys=20, clock=lambda: now[0])
|
||||
for i in range(300): # flood the cheap route
|
||||
rl.check("/v1/repos", f"10.1.{i // 256}.{i % 256}")
|
||||
allowed, _ = rl.check("/v1/link", "203.0.113.7") # never-seen client
|
||||
self.assertTrue(allowed, "/v1/link locked out by a /v1/repos flood")
|
||||
|
||||
def test_reclaim_uses_the_right_window_per_route(self):
|
||||
now = [1000.0]
|
||||
rl = granthi_link.RateLimiter(
|
||||
rules={"/v1/link": (1, 3600), "/v1/repos": (1, 10)},
|
||||
max_keys=2, clock=lambda: now[0])
|
||||
rl.check("/v1/repos", "a")
|
||||
rl.check("/v1/repos", "b")
|
||||
self.assertFalse(rl.check("/v1/repos", "c")[0])
|
||||
now[0] += 11 # past the /v1/repos window only
|
||||
self.assertTrue(rl.check("/v1/repos", "c")[0])
|
||||
|
||||
|
||||
class TestRateLimitConfigRound2(ServiceTestBase):
|
||||
def _svc(self, rl):
|
||||
cfg = dict(self.svc.cfg)
|
||||
cfg["rate_limit"] = rl
|
||||
return granthi_link.LinkService(cfg)
|
||||
|
||||
def test_booleans_are_rejected_in_rule_slots(self):
|
||||
"""bool subclasses int, so isinstance let [5, True] through as a
|
||||
1-SECOND window -- 5/hour silently became ~5/sec."""
|
||||
for bad in ([5, True], [True, 3600], [False, 3600], [5, False]):
|
||||
with self.subTest(rule=bad):
|
||||
with self.assertRaises(SystemExit):
|
||||
self._svc({"rules": {"/v1/link": bad}})
|
||||
|
||||
def test_valid_int_rule_still_accepted(self):
|
||||
svc = self._svc({"rules": {"/v1/link": [5, 3600]}})
|
||||
self.assertEqual(svc.limiter.rules["/v1/link"], (5, 3600))
|
||||
|
||||
def test_trusted_proxies_as_bare_string_refuses_startup(self):
|
||||
"""A string would be iterated character by character, each char
|
||||
treated as a network."""
|
||||
with self.assertRaises(SystemExit):
|
||||
self._svc({"trust_forwarded_for": True,
|
||||
"trusted_proxies": "10.0.0.0/8"})
|
||||
|
||||
def test_wildcard_trusted_proxy_refuses_startup(self):
|
||||
"""0.0.0.0/0 or ::/0 restores 'trust XFF from anyone' -- the exact
|
||||
hole trusted_proxies exists to close."""
|
||||
for wild in ("0.0.0.0/0", "::/0"):
|
||||
with self.subTest(cidr=wild):
|
||||
with self.assertRaises(SystemExit):
|
||||
self._svc({"trust_forwarded_for": True,
|
||||
"trusted_proxies": [wild]})
|
||||
|
||||
def test_non_string_proxy_entry_refuses_startup(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self._svc({"trust_forwarded_for": True,
|
||||
"trusted_proxies": [10, "10.0.0.0/8"]})
|
||||
|
||||
def test_host_address_without_prefix_is_accepted(self):
|
||||
svc = self._svc({"trust_forwarded_for": True,
|
||||
"trusted_proxies": ["127.0.0.1", "10.0.0.0/8"]})
|
||||
self.assertEqual(len(svc.trusted_proxies), 2)
|
||||
|
||||
Reference in New Issue
Block a user