The audit that produced these had every verifier agent die, so none were
confirmed. Checked each against the running system rather than guessing.
1. COOKIE Secure FLAG — REAL, fixed. The Cloudflare Tunnel runs OFF this box
(observed source 10.30.20.67, 155 requests in the journal) and uvicorn
only honours X-Forwarded-* from --forwarded-allow-ips, default 127.0.0.1.
Proven by hitting the LAN IP with X-Forwarded-Proto: https and watching
Secure vanish from Set-Cookie. Every internet visitor's session cookie
was going out without it.
Fixed in the unit drop-in with --proxy-headers and an allow-list scoped
to the tunnel host — NOT "*", because trusting that header from anywhere
would let a LAN client forge the IP the per-IP limiters key on. Verified
both directions: trusted source + header gets Secure, plain LAN http
correctly does not, and a spoof from an untrusted host is ignored.
2. DOUBLE GUEST ON REMOUNT — REAL but narrow, left alone. The guestAttempted
ref already covers StrictMode's double-effect (refs survive it). The only
hole is unmounting during the in-flight request, which needs navigating
away and back inside ~200ms and costs one unused row. Not worth
complicating the open door's happy path for.
3. SILENT REDIRECT WHEN RATE-LIMITED — REAL, fixed. A visitor whose guest
provisioning was refused got bounced to /enter with no explanation — and
at 5/hour/IP a household or cafe behind one NAT reaches that easily. The
failure reason (the backend's own in-fiction line) now rides along in
router state and /enter shows it, so nobody is silently handed a login
form they never asked for.
4. RATE LIMITER KEYS NEVER EVICTED — REAL, fixed. defaultdict entries
survived forever even once their hit list emptied. The open door made
this materially worse: every visitor is now a real account, so every
visitor permanently added a key across eleven limiter instances. Added an
opportunistic sweep every 512 admitted calls — no background task, cost
lands on whoever generates the load. Three tests; verified they catch it
by disabling the sweep and watching one fail.
5. SUMMON RACE vs TELEMETRY — REAL, fixed. Nothing serialised summoning.
_handle_anomaly checks `state.entity is None` then awaits a summon
containing a multi-second LLM mint, and the ESP32's HTTP ingestion path
calls _handle_anomaly on the SAME SeanceState — which is the entire point
of the device integration. Both could pass the check: two entities
minted, two essence credits, two item rolls, state.entity clobbered by
whichever finished last. Now guarded by a per-session asyncio.Lock.
6. LEGACY ENTITIES STUCK AT DEFAULT TRAITS — mechanism REAL, zero rows
affected here. The ALTER defaults traits to '{}' with no backfill and
roll_traits only runs at mint, so a pre-migration spirit would read 0.5
for everything — making `trust` always correct and `cross_over`
unreachable. This install has 0 such rows. Added a signature-seeded
backfill anyway, guarded to empty-traits rows so it can never touch a
spirit that already has a real nature.
(A seventh claim from the same batch — that iOS EMF is silently dead — was
refuted earlier and deliberately left untouched.)
34 targeted tests pass; deployed and verified live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
from unittest.mock import patch
|
|
|
|
from app.rate_limit import RateLimiter, resolve_client_ip
|
|
|
|
|
|
def test_allows_up_to_limit_then_blocks():
|
|
limiter = RateLimiter(max_requests=3, window_seconds=60)
|
|
assert limiter.allow("user-1") is True
|
|
assert limiter.allow("user-1") is True
|
|
assert limiter.allow("user-1") is True
|
|
assert limiter.allow("user-1") is False
|
|
|
|
|
|
def test_different_keys_tracked_independently():
|
|
limiter = RateLimiter(max_requests=1, window_seconds=60)
|
|
assert limiter.allow("user-1") is True
|
|
assert limiter.allow("user-2") is True
|
|
assert limiter.allow("user-1") is False
|
|
|
|
|
|
def test_hits_expire_after_window_elapses():
|
|
limiter = RateLimiter(max_requests=2, window_seconds=10)
|
|
|
|
with patch("app.rate_limit.time.monotonic", return_value=100.0):
|
|
assert limiter.allow("user-1") is True
|
|
assert limiter.allow("user-1") is True
|
|
assert limiter.allow("user-1") is False # at limit
|
|
|
|
with patch("app.rate_limit.time.monotonic", return_value=111.0):
|
|
# 11 seconds later, both prior hits (at t=100) are older than window_start (111 - 10 = 101)
|
|
assert limiter.allow("user-1") is True
|
|
|
|
|
|
def test_resolve_client_ip_prefers_cf_connecting_ip_over_socket_peer():
|
|
# The App CT sits behind a Cloudflare Tunnel on a separate machine — the
|
|
# raw socket peer is always the tunnel, never the visitor, for every
|
|
# internet-facing request.
|
|
headers = {"cf-connecting-ip": "203.0.113.7"}
|
|
assert resolve_client_ip(headers, "10.30.20.1") == "203.0.113.7"
|
|
|
|
|
|
def test_resolve_client_ip_falls_back_to_socket_peer_without_header():
|
|
# Direct LAN/local access (no Cloudflare in front) has no such header.
|
|
assert resolve_client_ip({}, "10.30.20.1") == "10.30.20.1"
|
|
|
|
|
|
def test_resolve_client_ip_falls_back_to_unknown_with_no_peer_or_header():
|
|
assert resolve_client_ip({}, None) == "unknown"
|
|
|
|
|
|
# --- key eviction -----------------------------------------------------------
|
|
|
|
|
|
class TestKeyEviction:
|
|
"""Keys must not accumulate forever.
|
|
|
|
The open door provisions a real account per visitor, so every visitor
|
|
contributes a distinct user-id key to each limiter. Without eviction a
|
|
long-running process grows without bound.
|
|
"""
|
|
|
|
def test_stale_keys_are_swept(self, monkeypatch):
|
|
limiter = RateLimiter(max_requests=5, window_seconds=60)
|
|
clock = {"t": 1000.0}
|
|
monkeypatch.setattr("app.rate_limit.time.monotonic", lambda: clock["t"])
|
|
|
|
# A burst of one-shot visitors, each seen exactly once.
|
|
for i in range(RateLimiter.SWEEP_EVERY):
|
|
limiter.allow(f"visitor-{i}")
|
|
assert limiter.tracked_keys == RateLimiter.SWEEP_EVERY
|
|
|
|
# Long after their window has closed, one more call triggers a sweep.
|
|
clock["t"] += 10_000
|
|
for i in range(RateLimiter.SWEEP_EVERY):
|
|
limiter.allow(f"later-{i}")
|
|
|
|
# The original cohort is gone; only the recent one is retained.
|
|
assert limiter.tracked_keys <= RateLimiter.SWEEP_EVERY + 1
|
|
|
|
def test_sweeping_never_forgets_an_active_key(self, monkeypatch):
|
|
"""A sweep must not hand someone a fresh budget mid-window."""
|
|
limiter = RateLimiter(max_requests=2, window_seconds=60)
|
|
clock = {"t": 500.0}
|
|
monkeypatch.setattr("app.rate_limit.time.monotonic", lambda: clock["t"])
|
|
|
|
assert limiter.allow("steady") is True
|
|
assert limiter.allow("steady") is True
|
|
assert limiter.allow("steady") is False
|
|
|
|
# Force many sweeps while "steady" stays inside its window.
|
|
for i in range(RateLimiter.SWEEP_EVERY * 2):
|
|
clock["t"] += 0.001
|
|
limiter.allow(f"noise-{i}")
|
|
|
|
# Still blocked — the sweep must not have dropped a live key.
|
|
assert limiter.allow("steady") is False
|
|
|
|
def test_a_key_recovers_normally_after_its_window(self, monkeypatch):
|
|
limiter = RateLimiter(max_requests=1, window_seconds=10)
|
|
clock = {"t": 0.0}
|
|
monkeypatch.setattr("app.rate_limit.time.monotonic", lambda: clock["t"])
|
|
|
|
assert limiter.allow("seeker") is True
|
|
assert limiter.allow("seeker") is False
|
|
clock["t"] += 11
|
|
assert limiter.allow("seeker") is True
|