# Security

How Zuzufy addresses the threats listed in the specification. File references point to the
enforcing code; most controls are covered by `tests/auth.test.js`, `tests/access.test.js`,
`tests/websocket.test.js` and `tests/rss.media.test.js`.

## Authentication & sessions

| Control | Implementation |
|---|---|
| Password hashing | bcrypt, cost 12 (`services/authService.js`). Plaintext is never stored or logged. |
| Password policy | ≥ 10 characters with letters and digits (`utils/validate.js`). |
| Sessions | 256-bit random token in an `HttpOnly`, `SameSite=Lax`, `Secure` (prod, `__Host-` prefix) cookie. Only its SHA-256 is stored, so a DB leak does not leak sessions. Server-side revocation on logout, password change/reset, suspension. |
| Brute force / credential stuffing | Per-IP rate limits on login/reset/2FA (`middleware/rateLimit.js`) **and** per-account lockout after 5 failures for 15 min. Uniform error messages and constant-time dummy hash compare prevent account enumeration; forgot-password always returns the same response. |
| Email verification & reset | Single-use, hashed, expiring tokens (`auth_tokens`). Resetting a password revokes all sessions. |
| 2FA | TOTP (RFC 6238, ±1 step) via speakeasy with QR enrolment; secrets AES-256-GCM encrypted at rest (`utils/crypto.js`). Admins can be forced to use it (`REQUIRE_ADMIN_TOTP=1`). |
| Admin bootstrap | Only while no admin exists; production additionally requires `ADMIN_BOOTSTRAP_TOKEN` (or use the `create-admin` CLI). |

## Request integrity

| Threat | Control |
|---|---|
| CSRF | Double-submit token (`zz_csrf` cookie echoed in `X-CSRF-Token`) on every unsafe method + Origin check + SameSite cookies (`middleware/csrf.js`). The Stripe webhook is exempt and verified by signature instead. |
| CORS | Same-origin API; allowed origins = `APP_URL` + `CORS_ORIGINS`. |
| Input validation | Declarative schemas drop unknown fields (no mass assignment), bound lengths, strip control characters (`utils/validate.js`). Route ids are parsed strictly. |
| SQL injection | Every query is parameterised (`mysql2` placeholders); dynamic ORDER BY uses fixed maps; FULLTEXT input is reduced to safe boolean terms. |
| XSS | The client builds DOM with `textContent` (`public/js/lib/dom.js` `h()`); user content is never assigned to `innerHTML`. Strict CSP (`script-src 'self'`, no inline scripts, `frame-ancestors 'none'`). RSS output is XML-escaped. Chat strips control and bidi-override characters. |
| Clickjacking / sniffing | `frame-ancestors 'none'`, `X-Content-Type-Options: nosniff`, HSTS in production (helmet). |
| Error leakage | Consistent `{ success:false, error:{code,message} }` envelope; stack traces never sent in production (`middleware/errorHandler.js`). |

## Authorization (IDOR)

All ownership and entitlement checks go through `services/accessControlService.js`
(`requireChannelManager`, `canUserAccessChannel`, `canUserAccessEpisode`, `canUserJoinLive`,
`canUserViewPremiumContent`). Media, playlists, schedules, episodes, plans, codes, analytics, live
sessions and chat moderation all resolve the parent channel and check the caller manages it (or is
an admin). Playlists and episodes can only reference media from the same channel. Premium playback,
P2P manifests and live joins check membership tier server-side.

## Uploads & media

- Streaming multipart parsing with hard size limits enforced while streaming (`media/uploadHandler.js`).
- Type is decided by magic bytes, then confirmed by ffprobe; client MIME/extension are not trusted.
- Files are stored under server-generated UUID keys; original filenames are display-only metadata.
  Storage keys are validated against a strict pattern and resolved under the storage root (path
  traversal impossible) (`storage/StorageService.js`).
- FFmpeg/ffprobe run via `spawn` with argument arrays — never a shell, never user-supplied paths.
- Optional malware scanning hook (`MALWARE_SCAN_COMMAND`).
- Private media is reachable only through expiring signed URLs; `tmp/` is never served.

## YouTube import

- Only links to a single YouTube video are accepted; they are canonicalised to
  `https://www.youtube.com/watch?v=<11-character id>` (playlists, other hosts, credentials in URLs,
  odd ports and anything else are refused) before reaching yt-dlp.
- yt-dlp runs via `spawn` with an argument array and `--` before the URL — no shell, and the URL can
  never be read as an option. Duration and file size are capped; live streams are refused.
- **Rights attestation:** the creator must confirm they own the content / hold all rights and accept
  full responsibility. The API rejects imports without `rightsConfirmed: true` and the current
  `termsVersion`; the exact text, version, user, time, source URL and hashed IP are stored in
  `media_rights_attestations` and the action is written to the audit log.

## Merch (Torvali)

- Connecting a store proves ownership by signing in to Torvali once (`POST /api/auth/login` → `GET /api/me`)
  with the store-owner account; employee accounts and inactive stores are refused. The password, TOTP
  code and Torvali token are used for that one request only and never stored or logged.
- A store already connected to another creator's channel cannot be claimed (409). All shop endpoints
  require channel-manager access; connect/disconnect/pin are audit-logged; connect is rate limited.
- Featured/pinned product ids are validated against the store's active catalogue.
- `/go/shop/:channelId/:productId` only ever redirects to the configured Torvali origin (no open
  redirect); product image URLs must be Torvali-relative or `https://`, and the CSP `img-src` allows
  only the Torvali origin in addition to our own.

## Realtime

- WebSocket upgrades require an allowed `Origin`, authenticate with the session cookie, cap
  connections per IP, cap payload size (16 KB), rate-limit messages per connection (closes on abuse)
  and process messages sequentially. Internal hooks cannot be invoked by clients.
- Chat: verified email required, slow mode, flood limits, duplicate suppression, 500-character limit,
  mute/ban with audit (`moderation_actions`), reports.
- P2P: peers only receive manifests they are entitled to; every chunk is SHA-256 verified before use
  or re-sharing; peers that send bad data are dropped; signalling is relayed only within the swarm
  and rate limited; peer ids are random (no user identity). Listeners can opt out; the preference text
  discloses IP visibility to peers. STUN only (no TURN relay of P2P traffic).
- Live: hosts must manage the channel; guests need an unguessable, hashed, expiring invite token; the
  host can mute, disable cameras and remove guests in real time.

## Push notifications

Web Push with VAPID; payloads are encrypted end-to-end (aes128gcm) so push services cannot read them.
Only HTTPS push endpoints are accepted; endpoints are stored per user with a hash index, expired ones
(404/410) are deleted and repeatedly failing ones pruned. Notification clicks only open same-origin URLs.

## Payments

No card data touches Zuzufy: hosted Stripe Checkout/Portal. Webhooks are signature-verified on the
raw body and de-duplicated (`payment_events`). Only provider ids and amounts are stored.

## Logging & audit

Structured pino logs with redaction of passwords, tokens, cookies, TOTP codes and card fields.
`audit_logs` records security-relevant actions (logins, failures, lockouts, 2FA changes, role and
status changes, publishing, broadcast state, moderation, payouts) with hashed IPs only.

## Privacy

Analytics store coarse device/browser/OS families, optional proxy-provided country, and no raw IP
addresses. Listening sessions are keyed by random ids for anonymous listeners.

## Reporting a vulnerability

Email the platform administrators listed in Admin → Administrators. Please do not test against other
users' accounts or data.
