FeedEcho v1.2.0 — Security Hardening (Critical + High)
Addresses all Critical and High-severity findings from the GPT-5.6 Terra Pro code review.
Critical Fixes
C-1: SSRF bypass via redirects
Replaced follow_redirects=True with a manual redirect loop that validates every Location header via validate_outbound_url(). A public feed that redirects to 169.254.169.254 or 127.0.0.1 is now blocked. Capped at 5 redirects.
C-2: Unauthenticated /oauth/connect SSRF
Removed /oauth/connect from auth-exempt paths. Only /oauth/callback remains exempt (needed for Mastodon redirect). Unauthenticated users can no longer trigger outbound requests to arbitrary instance URLs.
High Fixes
H-1: Session-bound, expiring, single-use OAuth state
New oauth_states table stores nonce, instance, hashed session binding, expiry, and consumed timestamp. State tokens are:
- Session-bound: OAuth session cookie ties callback to initiating browser
- Expiring: 10-minute TTL
- Single-use: Atomically consumed via conditional UPDATE
- Full HMAC: SHA-256 (was truncated to 64 bits)
H-2: Cursor stops at first failed item
break on first failure instead of continuing past it. Failed items are retried on next poll, never permanently skipped. Cursor only advances across a contiguous successful prefix.
H-3: Atomic pending-row claim
INSERT ... ON CONFLICT DO UPDATE with conditional WHERE clause. claim_token ensures only the owning worker can finalize. Stale pending rows (10+ min) are reclaimable. attempt_count tracks retries.
H-4: Per-feed lease
Atomic lease acquisition prevents concurrent processing. Lease renewed per-item, released in finally block. Cursor updates require lease ownership. Scheduler uses max_instances=1, coalesce=True.
H-5: SSRF validation for all outbound URLs
Renamed validate_feed_url to validate_outbound_url and applied to: feed URLs, Mastodon API calls, OAuth registration/token exchange. Blocks private IPs, embedded credentials, non-http schemes.
Also Fixed (Medium severity)
- M-1:
get_db()now properly closes connections via@contextmanager - M-1:
busy_timeout=30sfor SQLite write contention - M-1: WAL mode set once in
init_db, not per-connection - M-2: Generic error messages to clients; details logged server-side with
logger.exception()
Tests
71 tests passing. New tests cover: single-use state consumption, session binding, expired state rejection, full-length signature, redirect-to-private blocked, instance URL SSRF, embedded credentials.
Full changelog: v1.1.0...v1.2.0