Files
JakeBreath f86eccf9a3 Security fixes: SSRF, staff role escalation, SPA-only gating, throttling, encrypted keys
Findings from the audit (50-check harness across guest/user/uploader/staff/
admin) and their fixes:

- SSRF: 'Download to Library' and the staged-upload resolve path fetched
  any http(s) URL. services.validate_remote_url now enforces the e621
  media allowlist and open_remote re-validates every redirect hop; the
  download-task create endpoint and the guest proxy use them, so internal
  addresses (127.0.0.1, LAN, metadata) are rejected with 400.
- Privilege escalation: staff could promote users to staff and demote
  other staff. Role changes across the staff boundary now require an
  admin, matching the account-deletion rules; the Users page hides what
  the backend would refuse.
- SPA-only gating: /api/storage/ and /api/duplicates/* were readable by
  any authenticated account (absolute paths, duplicate groups) while the
  SPA only shows them to uploaders. They now require CanUpload.
- Throttling (REST_FRAMEWORK, env-overridable, counted in Redis):
  anon 120/min, user 600/min, login 5/min, register 20/hour, guest e621
  proxy 60/hour. Login now goes through a throttled view.
- e621 API keys are encrypted at rest with a Fernet key derived from
  SECRET_KEY (apps/accounts/crypto.py); a data migration encrypts existing
  rows and the column widens first. Reads decrypt transparently, legacy
  plaintext still works, and a changed SECRET_KEY reads as 'not
  configured' instead of leaking. Rotating SECRET_KEY now invalidates
  stored keys as well as signed media URLs.
- Hardening: the server refuses to start with DEBUG=False while SECRET_KEY
  is still the development default.

Verified: corrected harness 50/50 (guest visibility, IDOR, signed-URL
tamper/expiry, staged-upload/similarity privacy, role matrix, SSRF),
login throttles at the 6th attempt with 429, anon polling unaffected, the
guest proxy still reaches allowlisted hosts, live e621 auth works with the
decrypted key, and DB rows hold only ciphertext.
2026-09-18 00:21:14 -05:00

45 lines
1.5 KiB
Bash

SECRET_KEY=change-me-to-a-long-random-string
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
# Rate limits (per IP for anonymous, per account when signed in). Counted in
# the shared Redis cache; the defaults fit the shell's polling.
# THROTTLE_ANON=120/min
# THROTTLE_USER=600/min
# THROTTLE_LOGIN=5/min
# THROTTLE_REGISTER=20/hour
# THROTTLE_E621_PROXY=60/hour
# Cross-origin frontends (comma separated). Same-origin keeps working with an
# empty list; add e.g. https://j621.example.com when the SPA is served from
# another origin than this API.
# CORS_ALLOWED_ORIGINS=https://j621.example.com
# CORS_ALLOW_ALL_ORIGINS=false
# CORS_ALLOW_CREDENTIALS=false
# CSRF_TRUSTED_ORIGINS=https://j621.example.com
# Trust X-Forwarded-Proto/Host from a TLS-terminating reverse proxy so media
# URLs keep https and the public hostname.
# TRUST_PROXY_HEADERS=true
# Absolute path to your media folder, or relative to the backend/ folder.
WATCHED_FOLDER=media/library
# MariaDB (docker compose at the repo root)
DB_HOST=127.0.0.1
DB_PORT=3307
DB_NAME=j621
DB_USER=j621
DB_PASSWORD=j621
# Redis (docker compose at the repo root)
REDIS_URL=redis://127.0.0.1:6380/1
# Guest visibility: e621's anonymous default blacklist is mirrored into
# Redis by `manage.py refresh_guest_blacklist`.
# GUEST_BLACKLIST_FALLBACK=young,cub,shota,loli,child,underage
# GUEST_BLACKLIST_TTL=3600
# Ephemeral similarity checks: temp files are wiped on startup and after
# this many minutes.
# SIMILARITY_TTL_MINUTES=30