Skip to content

fix(security): address security audit findings - #50

Merged
florianjs merged 8 commits into
mainfrom
fix/security-audit
Oct 8, 2026
Merged

florianjs merged 8 commits into
mainfrom
fix/security-audit

Conversation

@florianjs

Copy link
Copy Markdown
Owner

Summary

Security audit fixes, one commit per area. Unit tests 80 → 109 passing, production build passes.

⚠️ Not tested end-to-end against a real Postgres/Redis (no database available while working on this). Please smoke-test login, admin actions, upload/download and tracker announces on a staging instance before releasing.

Critical

  • Panic mode (fix(panic))
    • The key is now derived from the submitted panic password. Before, it came from the hash stored in the clear in the same DB.
    • Each value gets its own random AES-GCM IV. Before, one IV was reused for every field.
    • Encrypt and restore each run in a single transaction.
    • Both endpoints are rate limited. Encrypt now requires an admin session.
    • The legacy format can still be restored.
    • The ADMIN_API_KEY guard is removed. It allowed anonymous encryption whenever NODE_ENV != production, and returned 503 in production.
  • Stored XSS (fix(content))
    • Torrent descriptions (marked → v-html) are now sanitized with DOMPurify.
    • Admin rich text is sanitized server-side with sanitize-html.

Auth / authorization (fix(auth))

  • Roles and bans: read from the DB on every request instead of trusting the sealed session cookie. Demoted admins lost nothing before.
  • Sessions: cookies now expire after 7 days and are secure in production.
  • Client IP: proxy headers are only honoured from trusted proxies (TRUSTED_PROXIES). Before, IP bans and rate limits could be bypassed by spoofing X-Forwarded-For or CF-Connecting-IP.
  • Login and register:
    • challenges and PoW are consumed atomically;
    • the login proof is compared in constant time;
    • username enumeration is fixed (stable fake salt, identical errors);
    • login and register are rate limited.
  • Registration: atomic, so one invite creates one account and only one first admin can exist.
  • Passkey: removed from the client session and read from the DB.
  • Moderators: can no longer ban or unban other moderators. Removing the last admin is blocked.

Data leaks (fix(content))

  • The raw .torrent blob (which carried the uploader's passkey) is no longer returned in JSON. Announce URLs are stripped on upload.
  • Pending torrents are hidden from non-owners in details, uploads and downloads.
  • RSS now requires a session or ?passkey= and only lists approved torrents. The CDATA terminator is escaped in RSS and Torznab.
  • Uploaded files (SVG) are served under a CSP sandbox. Path traversal in logo/favicon cleanup is fixed.
  • Forum reads require login. Torrent uploads have a 5 MB limit and validated inputs. rateLimit() calls that were never awaited are now awaited.

Tracker (fix(tracker))

  • Credited upload is capped by elapsed time (TRACKER_MAX_RATE_MIBPS, default 100). Nothing is credited when there is no leecher.
  • Each peer is bound to its owner, so a reused peer_id can no longer skew another user's stats.
  • Announces on pending torrents are refused, except for the uploader and staff.
  • left=0 no longer bypasses the minimum ratio without a real snatch.
  • Scrape requires a passkey and explicit info_hash values. Full scrape is disabled.

Other

  • fix(invites): creating more invites than allowed is no longer possible.
  • build(deps): npm update + npm audit fix.
    • Advisories go from 71 to 28, with none high/critical left in code that runs in production.
    • Updated: h3, nitropack, nuxt 4.6, vue, ws, drizzle-orm, tiptap.
    • The Docker image moves to Node 22.
  • fix(infra):
    • Random Grafana password instead of admin.
    • Compose fails at startup when a secret is missing.
    • Data services are bound to localhost in the dev/local/loadtest compose files.
    • Passkeys are stripped from Caddy logs, and the DB URL is masked in logs.
    • Script injection in release-tag.yml is fixed.
    • userlist.txt is no longer tracked.
  • fix(api): /api/health no longer returns internal errors. reject only deletes pending torrents.

Deployment notes

  • Rotate NUXT_SESSION_PASSWORD. Old cookies have no expiry and still contain the passkey.
  • New env vars: TRUSTED_PROXIES, TRUST_CLOUDFLARE, TRACKER_TRUST_PROXY, TRACKER_MAX_RATE_MIBPS (see .env.example).
  • IP_HASH_SECRET: in production, a value that is the placeholder or shorter than 32 characters makes IP hashing throw.
  • Grafana: an existing volume keeps its old password. Change it manually if it was admin.
  • Behaviour changes:
    • RSS feeds need ?passkey=;
    • the panic button asks for the panic password;
    • ADMIN_API_KEY is gone.

Not addressed

  • Login still accepts the stored verifier as a credential (pass-the-hash). A real fix needs SRP or OPAQUE.
  • No setup token: the first user to register becomes admin.
  • Hit-and-run can be bypassed by never sending completed.
  • No per-user peer cap and no per-passkey rate limit on the tracker.
  • Raw IPs, and passkeys used as Torznab keys, are stored in Redis.
  • No per-user session revocation. The CSP still allows unsafe-inline.
  • Major dependency upgrades are not done (pinia 4, vue-router 5, ioredis 6, marked 18, vitest 5, webtorrent 3).
  • With Nuxt 4.6, noUncheckedIndexedAccess is enabled. The typecheck now reports 33 errors instead of 25. They are on lines that already existed, and typecheck is not run in CI.

Test plan

  • npx vitest run: 109 passing, with new tests for panic crypto, client IP, sanitizers, tracker credit and torrent announce stripping
  • npm run build
  • The production server boots with no import errors (no DB available)
  • docker compose config passes for prod, local and dev; caddy adapt passes
  • On staging, smoke-test:
    • login and register;
    • demoting an admin;
    • upload, download and announce;
    • RSS with a passkey;
    • panic encrypt and restore

🤖 Generated with Claude Code

florianjs and others added 8 commits October 8, 2026 11:50
- derive the key from the submitted panic password, not the stored hash
- use a fresh random IV per encrypted value (iv:ct:tag)
- run encrypt and restore in a single transaction, never half restore
- keep decrypting the legacy shared-IV format for existing instances
- rate limit encrypt and the public restore endpoint
- drop the API-key admin guard that allowed anonymous access outside production

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- read isAdmin/isModerator/isBanned from the DB on every request instead of
  trusting the sealed session cookie
- bound session lifetime (7 days) and set secure cookies in production
- honour proxy IP headers only from trusted proxies (TRUSTED_PROXIES)
- consume login challenges and PoW atomically, compare proofs in constant time
- stable fake salts and identical errors to stop username enumeration
- rate limit login and registration
- make registration atomic: one invite per account, single first admin
- keep the tracker passkey out of the client session, read it from the DB
- gate torrent downloads on approval, make the debug torrent endpoint admin only
- moderators can no longer ban or unban moderators, block removing the last admin
- escape LIKE wildcards in searches, hide raw IPs from moderators
- refuse the placeholder IP_HASH_SECRET in production

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sanitize torrent descriptions (marked output) with DOMPurify before v-html
- sanitize admin rich text (branding, homepage) server side with sanitize-html
- never return the raw .torrent blob in JSON, strip uploader announce URLs on upload
- hide pending torrents from non-owners in details, uploads and downloads
- require a session or passkey for RSS feeds, list approved torrents only
- escape CDATA terminators in RSS and Torznab descriptions
- sandbox uploaded files (SVG) with CSP, block path traversal on logo/favicon cleanup
- require login for forum reads, cap unbounded forum queries
- limit torrent upload size, validate description, category and tags
- authors can no longer delete topics with replies from others
- await rate limiters that were silently never applied

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- cap credited upload/download by elapsed time (TRACKER_MAX_RATE_MIBPS)
- credit no upload when the swarm has no other leecher, ignore non-finite counters
- bind Redis peer entries to their owner so peer_id reuse cannot skew or evict
- refuse announces on pending torrents except for the uploader and staff
- stop trusting left=0 to bypass the minimum ratio without a real snatch
- require a passkey and explicit info_hash for scrape, report approved torrents only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- npm update + npm audit fix: 71 -> 28 advisories, none left in runtime code
  (h3, nitropack, nuxt 4.6, vue 3.5.43, ws, drizzle-orm, tiptap, devalue, defu)
- drop stub @types/marked and @types/uuid, both packages ship their own types
- node 20 is end of life: use node 22 in the Docker image and docs workflows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- generate a random Grafana admin password instead of admin/admin
- fail fast in compose when DB, Redis, session, IP hash or Grafana secrets are unset
- bind Postgres, Redis, Prometheus and Grafana to localhost in dev/local/loadtest compose
- disable tracker X-Forwarded-For trust where the tracker port is published directly
- strip passkey/apikey query params from Caddy access logs
- mask credentials when db-push prints the database URL
- stop interpolating PR branch names in release workflow scripts, validate the version
- untrack pgbouncer userlist.txt (ship an example), drop the unused admin API key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- health check no longer returns raw database/redis error messages
- grafana password endpoint logs upstream errors instead of returning them
- reject only deletes pending torrents and bounds the reason

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 104 files, which is 4 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 731528a3-70ff-4feb-a879-dca511147b78
📥 Commits

Reviewing files that changed from the base of the PR and between 5ba04ec and ace5002.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (104)
  • .env.example
  • .github/workflows/deploy-docs.yml
  • .github/workflows/docs.yml
  • .github/workflows/release-tag.yml
  • .gitignore
  • Dockerfile
  • app/components/EditTorrentModal.vue
  • app/components/UploadTorrentModal.vue
  • app/components/admin/Panic.vue
  • app/components/admin/Users.vue
  • app/layouts/default.vue
  • app/pages/torrents/[hash].vue
  • app/types.d.ts
  • app/utils/markdown.ts
  • docker-compose.loadtest.yml
  • docker-compose.local.yml
  • docker-compose.prod.yml
  • docker-compose.yml
  • docker/caddy/Caddyfile
  • docker/pgbouncer/userlist.txt.example
  • nuxt.config.ts
  • package.json
  • scripts/db-push.sh
  • scripts/install.sh
  • scripts/setup-local-prod.sh
  • scripts/setup.sh
  • server/api/admin/categories/[id].put.ts
  • server/api/admin/categories/index.post.ts
  • server/api/admin/categories/seed.post.ts
  • server/api/admin/debug/redis-all-peers.get.ts
  • server/api/admin/debug/redis-peers.get.ts
  • server/api/admin/debug/tracker-status.get.ts
  • server/api/admin/favicon.post.ts
  • server/api/admin/logo.post.ts
  • server/api/admin/panic/encrypt.post.ts
  • server/api/admin/panic/index.get.ts
  • server/api/admin/panic/restore.post.ts
  • server/api/admin/system/grafana-password.post.ts
  • server/api/admin/users/[id]/ban.post.ts
  • server/api/admin/users/[id]/role.put.ts
  • server/api/admin/users/[id]/unban.post.ts
  • server/api/admin/users/index.get.ts
  • server/api/auth/challenge.get.ts
  • server/api/auth/login.post.ts
  • server/api/auth/passkey.get.ts
  • server/api/auth/register.post.ts
  • server/api/branding.get.ts
  • server/api/categories/index.get.ts
  • server/api/debug/[hash].get.ts
  • server/api/debug/torrent/[hash].get.ts
  • server/api/forum/categories/[id].get.ts
  • server/api/forum/categories/index.get.ts
  • server/api/forum/posts/[id].delete.ts
  • server/api/forum/posts/index.post.ts
  • server/api/forum/topics/[id].delete.ts
  • server/api/forum/topics/[id].get.ts
  • server/api/forum/topics/index.post.ts
  • server/api/health.get.ts
  • server/api/homepage-content.get.ts
  • server/api/invites/index.get.ts
  • server/api/invites/index.post.ts
  • server/api/mod/torrents/[hash]/approve.post.ts
  • server/api/mod/torrents/[hash]/reject.post.ts
  • server/api/mod/torrents/pending.get.ts
  • server/api/reports/index.post.ts
  • server/api/rss/category/[slug].get.ts
  • server/api/rss/latest.get.ts
  • server/api/tags/index.get.ts
  • server/api/torrents/[hash].delete.ts
  • server/api/torrents/[hash].get.ts
  • server/api/torrents/[hash]/download.get.ts
  • server/api/torrents/[hash]/index.patch.ts
  • server/api/torrents/[hash]/tags.put.ts
  • server/api/torrents/index.get.ts
  • server/api/torrents/index.post.ts
  • server/api/torznab/api/index.get.ts
  • server/api/torznab/utils/xml.ts
  • server/api/users/[id].get.ts
  • server/api/users/[id]/uploads.get.ts
  • server/api/users/hnr.get.ts
  • server/middleware/security.ts
  • server/redis/cache.ts
  • server/routes/uploads/[name].get.ts
  • server/tracker/credit.ts
  • server/tracker/handlers.ts
  • server/tracker/index.ts
  • server/utils/adminAuth.ts
  • server/utils/auth.ts
  • server/utils/clientIp.ts
  • server/utils/crypto.ts
  • server/utils/feedAuth.ts
  • server/utils/panic.ts
  • server/utils/pow.ts
  • server/utils/rateLimit.ts
  • server/utils/sanitize.ts
  • server/utils/schemas.ts
  • server/utils/torrentFile.ts
  • server/utils/validation.ts
  • tests/security/client-ip.test.ts
  • tests/security/panic.test.ts
  • tests/security/sanitize.test.ts
  • tests/security/torrent-file.test.ts
  • tests/security/tracker-credit.test.ts
  • types/auth.d.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@florianjs
florianjs merged commit c1eac5f into main Oct 8, 2026
1 check passed
@florianjs florianjs mentioned this pull request Oct 8, 2026
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.

1 participant