Skip to main content

Security

Threat model, secrets catalog, rate limiting, and incident response. Cryptographic details for auth tokens live in features/tokens.md; the scheme overview is in AUTH.md.


1. Threat model

Who we're defending against, in priority order:

  1. Scripted abusers — bots hammering /auth/register, /auth/login, or the submit endpoint. Rate-limited with @nestjs/throttler; self-registration closable via env.
  2. Opportunistic attackers with stolen tokens — refresh-token rotation + reuse detection catches most replays.
  3. Malicious authenticated users — permissions enforced via PermissionsGuard; soft-delete + audit trail on destructive actions.
  4. Supply-chain attacks on npm dependencies — lockfile + npm audit in CI (future); minimal dependency surface.
  5. Infrastructure compromise — out of scope for this doc; depends on the hosting provider.

Not in scope:

  • Nation-state-level attackers.
  • Custom malware on a user's device (we cannot protect tokens in chrome.storage.local against local malware — the OS-level threat is theirs, not ours).
  • DDoS at network layer (hosting provider concern).

2. Secrets catalog

Every secret the system handles, where it lives, and how it rotates.

SecretStorageHandlingRotation
JWT_ACCESS_SECRETPlatform secret manager in prod; api/.env in devOnly the API process reads itYearly or on compromise
JWT_REFRESH_SECRETSameSameYearly or on compromise
MONGO_URI (with password)SameOnly the API process reads itOn provisioning / incident
User passwordsHashed (argon2id) in users.passwordHashNever logged, never returned in responses, never compared via ==User-initiated; password-change endpoint
Refresh tokensHashed (SHA-256 via crypto.subtle.digest) in refresh_tokens.tokenHashThe plaintext only exists briefly on mint/rotate and in the client's chrome.storage.localRotate on every use
API keys (future)Hashed in api_keys.keyHashPlaintext shown once on creationUser-initiated; revoke via UI
Mongo Express basic-authME_USER / ME_PASSWORD in root .envDev only — never deployed to prodDev-team rotation

Rules

  • Never commit secrets. .env is git-ignored; .env.example has placeholders.
  • Never echo secrets. Exception: the API returns refresh tokens to the client once per refresh. That's the design.
  • Hash refresh tokens server-side. We use SHA-256 (not bcrypt/argon2) because (a) entropy is already 256 bits from crypto.randomBytes(32) — no brute force concern, and (b) lookup on login must be O(1), which a slow hash defeats.
  • Hash user passwords with argon2id. The entropy is user-chosen; the slow hash is the defense.

3. Input validation

Every controller input is a DTO validated by class-validator through the global ValidationPipe. The pipe is configured with:

  • whitelist: true — unknown properties are stripped silently... no wait, set via forbidNonWhitelisted: true, so unknown properties return 400. Matches current main.ts:9-14.
  • transform: true — strings become the declared type where valid (e.g. '5'5 on an @IsInt() field).

Never trust the client for:

  • Paginationoffset/limit are bounded (see features/pagination.md).
  • Status filter on /contentstatus=pending checked against permissions; else 403.
  • Slugs — regex enforced via @IsSlug().
  • URLs@IsUrl() on submission; follow-up fetch-based validation (if any) runs server-side, not client-side.
  • Tag slugs submitted on content — verified to exist and be active; unknown tags reject the whole submission.

4. Output handling

The API returns JSON only. No HTML, no server-rendered templates. Therefore:

  • XSS is the client's problem (sanitize embed HTML on render; see AgnosticUI.md §9).
  • SQL injection is not applicable — we use Mongoose with parameterized queries.
  • NoSQL injection: never pass unvalidated request bodies directly to .find() or .$where. ValidationPipe defeats most of this; the remainder is code review.

5. Rate limiting

@nestjs/throttler applied globally with per-route overrides. Starting limits (revisit after observing real traffic):

RouteLimit (unauthenticated)Limit (authenticated)
POST /auth/register5 / hour / IP
POST /auth/login10 / hour / IP
POST /auth/refresh60 / hour / IP
POST /content/submit30 / hour / user
GET /content120 / minute / IP600 / minute / user
GET /catalog, GET /metano limit (cached upstream)no limit

Exceeded → 429 with Retry-After header.

Do not rate-limit health/ready endpoints (probes would break).


6. CORS

Strict allow-list from CORS_ALLOWED_ORIGINS (comma-separated). See AUTH.md §2 CORS.

  • Development: include the unpacked extension's chrome-extension://<dev-id> and http://localhost:* if a dev web viewer exists.
  • Production: the published extension's chrome-extension://<web-store-id> only.
  • Never use Access-Control-Allow-Origin: * combined with auth. Tokens are bearer-only — no credentials cookies — so in principle * is safe, but strict allow-list is the defensive default.

7. Dependency auditing

  • pnpm-lock.yaml committed.
  • pnpm audit in CI (future). Fail build on high or critical.
  • Renovate or Dependabot opens PRs for updates. Human review before merge.
  • Minimize the dependency surface — every new dependency is a supply-chain asset.

8. Account security UX

These features are not in scope for v1 but are called out so security decisions don't paint us into a corner:

  • Email verification on registration — if/when added, User gets emailVerifiedAt. Until then, self-registration treats the email as trusted-after-submit.
  • 2FA (TOTP) — fits cleanly into the Chrome extension (scan QR in the options page; prompt in login flow). Schema reservation: User.twoFactor: { enabled: boolean; secret?: string; recoveryCodes?: string[] }.
  • Password-change revokes sessions — already documented in AUTH.md §6. Implement this in v1 so we don't have to retrofit revocation later.

9. Incident response

For specific scenarios (refresh-token compromise, rate-limit flood), see OPERATIONS.md §7.

General incident checklist

  1. Identify. What's the symptom? What's the blast radius? Who's affected?
  2. Contain. Revoke affected tokens. Block abusive IPs. Disable affected endpoints if needed.
  3. Communicate. Status page (TBD). If users are materially affected, email them.
  4. Remediate. Fix the underlying cause. Deploy.
  5. Postmortem. Within one week: what happened, timeline, what detection-caught-first vs could-have-caught-first, action items. Blameless.

Security-sensitive events that must be logged at error level

  • auth.refresh.reused — a refresh token that was already rotated was presented. Revoke the family; log the userId and the source IP.
  • auth.password.bruteforce — an account hit the login rate limit N times.
  • permissions.escalation_attempted — a user called an endpoint requiring a permission they lack. (Distinct from a normal 403; log only if repeated or from a user who should have known better.)

Alerts on these events (via whatever alerting is wired) are higher-priority than 5xx alarms.


10. Reporting a vulnerability

Placeholder — when we have a public domain: security@<domain>. Until then, the maintainer's direct channel.

Please give us 90 days before public disclosure.