Skip to content

feat: User-Agent gate for known-client strict mode#157

Merged
jakebromberg merged 1 commit into
mainfrom
feature/155-ua-gate
Jun 1, 2026
Merged

feat: User-Agent gate for known-client strict mode#157
jakebromberg merged 1 commit into
mainfrom
feature/155-ua-gate

Conversation

@jakebromberg

Copy link
Copy Markdown
Member

Closes #155.

Summary

  • Adds a structural pre-check on POST /request: when STRICT_FINGERPRINT_FOR_KNOWN_CLIENTS=true and the request's User-Agent matches a registered WXYC client at-or-above its strict-mode version (currently WXYC-iOS >= 3.2), missing X-Device-Fingerprint triggers a 403 before parse and before the BS ban round-trip.
  • Default-off via the new env var so the code can deploy without disrupting any existing traffic; flip after iOS 3.2 reaches App Store rollout. Independent of ENFORCE_REQUEST_BANS — this is a structural requirement on known clients, not a ban decision.
  • Unknown UAs (curl, browsers, v3.1 iOS, anything not in the registry in services/ua_gate.py) keep the existing lenient contract. The registry is a one-line addition when WXYC-Android ships its own ban-aware version.
  • Telemetry uses ban_source='rom_strict_mode' + ban_reason='ua_gate_missing_fingerprint' so PostHog dashboards distinguish UA-gate rejections from real BS-driven bans (ban_source='bs' from Enforce request-line ban on POST /request via BS /auth/check-request-ban #150).
  • PostHog capture is wrapped in try/except mirroring Enforce request-line ban on POST /request via BS /auth/check-request-ban #150's shadow-ban posture: an ingest outage must not flip the 403 to a 500.

Why this exists

The BS ban check (#150) plugs every ban-evasion vector except "strip both Authorization and X-Device-Fingerprint headers." ROM's proceed-as-unauth on missing headers is required for v3.1 iOS in the App Store (which sends no headers), so we can't reject blank-header requests globally. The UA gate closes the gap for clients that should have headers — iOS 3.2+ unconditionally sends fingerprint, so a self-identified 3.2 UA without a fingerprint is by-construction evasion.

UA spoofing is in scope but not a blocker: a determined attacker who spoofs User-Agent: WXYC-iOS/3.2.0 then has to also send a fingerprint, at which point they're back on the BS fingerprint-banned path.

Test plan

  • 33 new unit tests in tests/unit/test_ua_gate.py:
    • Pure matcher: every UA category (none, empty, browser, curl, postman, requests; v3.0–v3.1 + v2.x + v1.x → False; v3.2 → True with bare/dotted/build/CFNetwork suffixes; v3.3 → True; v4.x → True; unknown product → False; lowercase product → False; mixed tokens; first-unknown-then-known)
    • Router wiring: flag-on + known-UA + no FP → 403, no Groq, no LML, no Slack, no BS round-trip
    • Router wiring: flag-on + known-UA + has FP → passes through to BS ban check (200 with BanCheckResult(banned=False))
    • Router wiring: flag-on + unknown UA + no FP → passes through (200)
    • Router wiring: flag-on + v3.1 UA + no FP → passes through (200)
    • Router wiring: flag-on + no UA → passes through (200)
    • Router wiring: PostHog ingest outage during the gate → still returns 403 (shadow-ban invariant)
    • Router wiring: flag-off + known UA + no FP → passes through (200), no request_blocked emitted
  • Existing 504-test suite unchanged and green.
  • ruff check, ruff format --check, mypy all green.

…entifies known WXYC client

Closes #155. Adds a structural pre-check that rejects 403 when the caller's User-Agent matches a registered WXYC client at-or-above its strict-mode version (currently WXYC-iOS >= 3.2) and X-Device-Fingerprint is absent. Independent of ENFORCE_REQUEST_BANS; default-off via STRICT_FINGERPRINT_FOR_KNOWN_CLIENTS so the code can deploy before iOS 3.2 reaches App Store rollout. Unknown UAs (curl, browsers, v3.1 iOS) keep the existing lenient contract.

Closes the only ban-evasion vector the BS ban check (#150) can't reach: a banned listener stripping both Authorization and X-Device-Fingerprint headers. iOS 3.2 will unconditionally send fingerprint + WXYC-iOS UA (WXYC/wxyc-ios-64#351), so after rollout completes, missing fingerprint from a self-identified 3.2+ client is by-construction evasion.

Telemetry uses ban_source='rom_strict_mode' + ban_reason='ua_gate_missing_fingerprint' so PostHog dashboards distinguish UA-gate rejections from real fingerprint/user bans (#150's ban_source='bs').
@jakebromberg jakebromberg merged commit dc93f6b into main Jun 1, 2026
7 checks passed
@jakebromberg jakebromberg deleted the feature/155-ua-gate branch June 1, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Strict-mode: reject missing X-Device-Fingerprint when User-Agent indicates iOS 3.2+

1 participant