title: "security"#
Overview of security mechanisms in plyr.fm.
Authentication#
We use HttpOnly Cookies for session management to prevent XSS attacks. See Authentication for details on the OAuth flow, token management, and environment architecture.
Rate Limiting#
We enforce application-side rate limits to prevent abuse. Limits are configured per-endpoint using slowapi with sensible defaults (e.g., 10 req/min for uploads, 30 req/min for API reads).
HTTP Security Headers#
The SecurityHeadersMiddleware in src/backend/main.py automatically applies industry-standard security headers to all responses:
Strict-Transport-Security(HSTS): Enforces HTTPS (Production only). Max-age set to 1 year.X-Content-Type-Options: nosniff: Prevents browsers from MIME-sniffing a response away from the declared content-type.X-Frame-Options: DENY: Prevents the site from being embedded in iframes (clickjacking protection).X-XSS-Protection: 1; mode=block: Enables browser cross-site scripting filters.Referrer-Policy: strict-origin-when-cross-origin: Controls how much referrer information is included with requests.
Supporter-Gated Content#
Tracks with support_gate set require atprotofans supporter validation before streaming.
Access Model#
request → /audio/{file_id} → check support_gate
↓
┌──────────┴──────────┐
↓ ↓
public gated track
↓ ↓
307 → R2 CDN validate_supporter()
↓
┌──────────┴──────────┐
↓ ↓
is supporter not supporter
↓ ↓
presigned URL (5min) 402 error
Storage Architecture#
- public bucket:
plyr-audio- CDN-backed, public read access - private bucket:
plyr-audio-private- no public access, presigned URLs only
when support_gate is toggled, a background task moves the file between buckets.
Presigned URL Behavior#
presigned URLs are time-limited (5 minutes) and grant direct R2 access. security considerations:
-
URL sharing: a supporter could share the presigned URL. mitigation: short TTL, URLs expire quickly.
-
offline caching: if a supporter downloads content (via "download liked tracks"), the cached audio persists locally even if support lapses. this is intentional - they legitimately accessed it when authorized.
-
auto-download + gated tracks: the
gatedfield is viewer-resolved (true = no access, false = has access). when liking a track with auto-download enabled:- supporters (
gated === false): download proceeds normally via presigned URL - non-supporters (
gated === true): download is skipped client-side to avoid wasted 402 requests
- supporters (
ATProto Record Behavior#
when a track is gated, the ATProto fm.plyr.track record's audioUrl changes:
- public: points to R2 CDN URL (e.g.,
https://cdn.plyr.fm/audio/abc123.mp3) - gated: points to API endpoint (e.g.,
https://api.plyr.fm/audio/abc123)
this means ATProto clients cannot stream gated content without authentication through plyr.fm's API.
Validation Caching#
currently, validate_supporter() makes a fresh call to atprotofans on every request. for high-traffic gated tracks, consider adding a short TTL cache (e.g., 60s in redis) to reduce latency and avoid rate limits.
CORS#
Cross-Origin Resource Sharing (CORS) is configured to allow:
- Localhost: For development (
http://localhost:5173). - Production/Staging Domains:
plyr.fm,stg.plyr.fm, and Cloudflare Pages preview URLs (via regex).
Configuration is managed in src/backend/config.py under FrontendSettings.