fix: verify all six unproven audit findings — four real, two not
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>
This commit is contained in:
@@ -22,16 +22,47 @@ def resolve_client_ip(headers: Mapping[str, str], direct_host: str | None) -> st
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""Fixed-window limiter keyed by an arbitrary string (user id or client IP)."""
|
||||
"""Fixed-window limiter keyed by an arbitrary string (user id or client IP).
|
||||
|
||||
Expired keys are swept periodically rather than left to accumulate.
|
||||
Without that, every distinct key ever seen stays in the dict forever —
|
||||
and since the open door provisions a real account per visitor, "every
|
||||
distinct key" now means every visitor, across all eleven limiter
|
||||
instances. That is a slow but genuine leak in a process designed to run
|
||||
for months.
|
||||
"""
|
||||
|
||||
# Sweep every N admitted calls rather than on a timer: no background
|
||||
# task to own, and the cost lands on whoever is generating the load. At
|
||||
# 512 the amortised cost is negligible, and a limiter can hold at most a
|
||||
# window's worth of traffic plus 512 stale keys.
|
||||
SWEEP_EVERY = 512
|
||||
|
||||
def __init__(self, max_requests: int, window_seconds: float):
|
||||
self.max_requests = max_requests
|
||||
self.window_seconds = window_seconds
|
||||
self._hits: dict[str, list[float]] = defaultdict(list)
|
||||
self._calls_since_sweep = 0
|
||||
|
||||
def _sweep(self, window_start: float) -> None:
|
||||
"""Drop keys whose most recent hit has fallen out of the window."""
|
||||
stale = [
|
||||
key
|
||||
for key, hits in self._hits.items()
|
||||
if not hits or hits[-1] < window_start
|
||||
]
|
||||
for key in stale:
|
||||
del self._hits[key]
|
||||
|
||||
def allow(self, key: str) -> bool:
|
||||
now = time.monotonic()
|
||||
window_start = now - self.window_seconds
|
||||
|
||||
self._calls_since_sweep += 1
|
||||
if self._calls_since_sweep >= self.SWEEP_EVERY:
|
||||
self._calls_since_sweep = 0
|
||||
self._sweep(window_start)
|
||||
|
||||
hits = self._hits[key]
|
||||
while hits and hits[0] < window_start:
|
||||
hits.pop(0)
|
||||
@@ -39,3 +70,8 @@ class RateLimiter:
|
||||
return False
|
||||
hits.append(now)
|
||||
return True
|
||||
|
||||
@property
|
||||
def tracked_keys(self) -> int:
|
||||
"""Live key count — exposed so the leak is testable."""
|
||||
return len(self._hits)
|
||||
|
||||
Reference in New Issue
Block a user