Skip to content

Security: authenticated session cookies, /api/asset SSRF fix, OAuth state#11

Open
gndclouds wants to merge 1 commit into
mainfrom
agent/maintenance/213a76e5
Open

Security: authenticated session cookies, /api/asset SSRF fix, OAuth state#11
gndclouds wants to merge 1 commit into
mainfrom
agent/maintenance/213a76e5

Conversation

@gndclouds

Copy link
Copy Markdown
Member

Summary

Fixes three confirmed vulnerabilities found in a defensive security review of the app. Each was verified against the actual code (data flow traced, not theoretical).

1. CRITICAL — Forgeable session cookies (src/lib/session-crypto.ts)

  • Sessions were encrypted with AES-256-CBC and no MAC/auth tag. CBC is malleable over the predictable JSON plaintext, so an attacker could tamper the decrypted userId and impersonate any user.
  • The secret fell back to a hardcoded "dev-secret-change-me" when SESSION_SECRET was unset — making every session forgeable.

Fix: switched to authenticated AES-256-GCM (tamper-evident via auth tag) and now require a real SESSION_SECRET ≥ 32 chars (no insecure fallback, validated lazily so the build doesn't break). Cookies not in the authenticated format — including the legacy iv:ciphertext form — are rejected, so existing users simply re-log in once.

2. HIGH — SSRF + token leak in GET /api/asset (src/app/api/asset/route.ts)

  • The route guarded an attacker-controlled ?url= with a substring check (url.includes("vercel-storage.com")). Payloads like http://169.254.169.254/?vercel-storage.com or http://vercel-storage.com.attacker.com/ passed and were fetched server-side — and the request carried the BLOB_READ_WRITE_TOKEN bearer header, leaking the blob token to an attacker host. The endpoint is unauthenticated.

Fix: parse the URL and enforce an https-only exact-hostname allow-list (vercel-storage.com or *.vercel-storage.com).

3. HIGH — OAuth login CSRF / session fixation (src/app/api/auth/login + callback)

  • The Are.na OAuth flow had no state parameter, enabling login CSRF (a victim silently logged into the attacker's Are.na account).

Fix: generate a random state, store it in an httpOnly cookie at /api/auth/login, and validate it with a constant-time compare on callback; the cookie is cleared afterward. (Shared cookie-name constant lives in src/lib/oauth.ts — Next.js route files can't export arbitrary symbols.)

Testing

  • npx tsc --noEmit — passes
  • npx eslint on changed files — passes
  • npx next build — compiles, type-checks, and generates all routes successfully
  • Standalone crypto check — GCM round-trips, rejects tampered cookies, and rejects the legacy unauthenticated format

Not included (recommended follow-ups)

These were also confirmed in the review but need their own change (new dependency and/or careful template work):

  • HIGH — Stored XSS via Are.na content_html / embed.html rendered through triple-stache {{{ }}} in templates (no HTML sanitization anywhere). Needs a sanitizer (e.g. sanitize-html) with an embed allow-list so media embeds keep working.
  • HIGH — dependency advisories: handlebars@4.7.8 (→ 4.7.9) and next@15.5.14 (→ latest 15.x). npm audit --omit=dev reports 1 critical / 5 high.
  • MEDIUM — no rate limiting on newsletter/subscribe and waitlist; session secure flag only set when NODE_ENV==="production" (plaintext on non-prod HTTP).
  • LOW<style> custom-CSS </style> breakout in the build path; cron secret compared with non-constant-time !==; Are.na access token stored in plaintext.

🤖 Generated with Claude Code

…h state

Three confirmed vulnerabilities found in a defensive review:

- Session cookies used AES-256-CBC with no MAC/auth tag, so the encrypted
  userId was malleable and forgeable, and the secret fell back to a hardcoded
  "dev-secret-change-me" string. Switch to authenticated AES-256-GCM (tamper-
  evident) and require a real SESSION_SECRET of >=32 chars (no insecure
  fallback). Legacy/unauthenticated cookies are now rejected. (critical)

- GET /api/asset gated its server-side fetch with a substring check
  (url.includes("vercel-storage.com")), so http://169.254.169.254/?vercel-storage.com
  and http://vercel-storage.com.attacker.com/ passed — SSRF plus leak of the
  BLOB_READ_WRITE_TOKEN bearer header to an attacker host. Replace with a parsed
  https-only exact-hostname allow-list. (high)

- Are.na OAuth had no state parameter -> login CSRF / session fixation. Bind the
  round-trip to a random state value in an httpOnly cookie, validated with a
  constant-time compare on callback. (high)

Verified: tsc --noEmit, eslint, full `next build`, and a crypto round-trip /
tamper-rejection check all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tiny-garden Ready Ready Preview, Comment Jun 15, 2026 11:05pm

Request Review

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