Skip to content

fix: address code-review findings on security hardening#327

Merged
fjbarrett merged 1 commit into
mainfrom
fix/hardening-review-findings
Jul 13, 2026
Merged

fix: address code-review findings on security hardening#327
fjbarrett merged 1 commit into
mainfrom
fix/hardening-review-findings

Conversation

@fjbarrett

Copy link
Copy Markdown
Owner

Summary

A post-merge review of #326 confirmed 10 issues, two of which would take production down on the next successful deploy (deploys are currently fail-closed on the missing DEPLOY_KNOWN_HOSTS secret, which is why prod is unaffected):

  • CSRF gate 403'd every same-origin mutation behind Caddy — the middleware compared Origin against req.nextUrl.origin, which under next start is the internal listen origin (https://localhost:3000). Now Sec-Fetch-Site is authoritative when present, with an Origin fallback checked against AUTH_URL / forwarded host.
  • DB TLS default broke the droplet's self-signed Postgressslmode=require with no CA flipped to rejectUnauthorized: true. Now follows libpq semantics: require/prefer encrypt without verifying; verify-ca/verify-full or DATABASE_CA_CERT verify.
  • Session revocation never applied to pre-deploy JWTs — versionless tokens were grandfathered on every request; they're now rejected (one-time re-auth for existing sessions).
  • Note request byte cap now covers MAX_NOTE_BODY UTF-16 chars at worst-case JSON encoding (6 bytes/char), so large multibyte notes stay saveable and editable.
  • CSP img-src additionally allows the legacy public-bucket origin (no-op unless S3 env is set) so pre-migration note images keep rendering.
  • Sign-out is single-session again, warns before discarding un-synced offline edits, and legacy IndexedDB pending ops migrate instead of being deleted.
  • AI metadata is opt-out (AI_METADATA_ENABLED=false) instead of opt-in, so a configured ANTHROPIC_KEY works again with no new env var.
  • Uploads edited out of every note are swept when a new upload would exceed the quota (60s grace for images awaiting autosave).

Known follow-up: iOS AuthClient still POSTs /api/auth/revoke on sign-out, so native sign-out remains "sign out everywhere".

Test plan

  • npm test — 151 passing (updated tests that encoded the old TLS/AI/legacy-DB behavior; added regressions for the byte cap, opt-out gate, proxy-shaped origin check, and legacy pending-op migration)
  • npm run build green
  • CSRF verified against next start: production-shaped mutations (public Origin, internal listen origin, with/without Sec-Fetch-Site) pass to auth; foreign-origin and cross-site requests still 403

🤖 Generated with Claude Code

A high-effort review of this branch confirmed ten issues; the two worst
would have taken production down at deploy:

- CSRF gate: trust Sec-Fetch-Site when the browser sends it, and compare
  Origin against the configured public origin (AUTH_URL / forwarded
  host) instead of req.nextUrl.origin — behind Caddy that is the
  internal listen origin (localhost:3000), so every same-origin
  mutation 403'd. Same double-check removed from isSameOriginMutation.
- DB TLS: follow libpq sslmode semantics — require/prefer encrypt
  without authenticating the server, so the droplet's self-signed
  Postgres keeps connecting; verify-ca/verify-full or DATABASE_CA_CERT
  turn verification on.
- Session revocation: stop grandfathering pre-versioning JWTs. They can
  never gain a persisted claim, so grandfathering exempted every
  pre-deploy session from revocation forever; they now re-auth once.
- Note size: the request byte cap now covers MAX_NOTE_BODY UTF-16 chars
  at worst-case JSON encoding (6 bytes/char) — large multibyte notes
  were unsaveable and existing ones uneditable.
- CSP: img-src additionally allows the legacy public-bucket origin so
  images embedded in pre-migration note bodies keep rendering.
- Sign-out: back to ending only this browser's session (revoke-all is a
  separate explicit call), warns before discarding un-synced offline
  edits, and legacy IndexedDB pending ops migrate again instead of
  being deleted.
- AI metadata: a configured ANTHROPIC_KEY enables it again;
  AI_METADATA_ENABLED=false is the explicit opt-out.
- Upload quota: uploads edited out of every note are swept when a new
  upload would otherwise exceed the quota, instead of leaking forever.

Verified against next start: mutations with the production request
shape (public Origin, internal listen origin) pass the CSRF gate and
reach auth; foreign-origin and cross-site requests still 403.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fjbarrett
fjbarrett merged commit e03c6f4 into main Jul 13, 2026
3 checks passed
@fjbarrett
fjbarrett deleted the fix/hardening-review-findings branch July 13, 2026 23:07
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