deckhearth/.convoys/secure-scanner-gemini-key.md
Randall Stillwell 0918bd0449 fix(security): stop leaking Gemini API key to browsers
Delete the public /api/config/gemini endpoint and remove client auto-load
paths so GEMINI_AI_API_KEY stays server-side only. Add a scan rate-limit
class for the upcoming server-side identify route and a CI gate that blocks
reintroducing config key leaks or new browser LLM URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 08:40:00 -05:00

4.9 KiB
Raw Blame History

name classification success_metric skip status created
secure-scanner-gemini-key server-only GEMINI_AI_API_KEY rotated; no endpoint returns the key to a browser; lib/rate-limit.js exposes a sixth scan class (5 req / 1 min, user-keyed); forbidden-client-side-llm-keys CI gate green.
ia
ux
visual
a11y
design
in-progress 2026-05-27

Convoy: secure-scanner-gemini-key

Stop leaking the Gemini API key to browsers and add a scan-specific rate limiter before the server-side scan pipeline lands in convoy #2.

Why

The scanner audit found that pages/api/config/gemini.js returns process.env.GEMINI_AI_API_KEY to any authenticated caller, and components/CameraScanner.js auto-fetches that endpoint on mount (lines 5367). A logged-in user — or anyone who obtains a session token — can read the production LLM key from DevTools and call Gemini directly, bypassing app rate limits and billing controls. This is a live secret exposure that must be closed before any further scanner work ships.

Scope

In scope

  • Delete pages/api/config/gemini.js — remove the key-returning endpoint entirely.
  • components/CameraScanner.js — remove the auto-load block at lines 5367 that fetches /api/config/gemini and stores the key client-side. The scanner will break temporarily until convoy #2 lands; that is acceptable for the security-only window.
  • lib/rate-limit.js — add a sixth scan entry to LIMITER_CONFIG (5 req / 1 min, user-keyed) and export checkScanRateLimit(req, userId) following the Decision-2 hybrid named-limiter pattern from .convoys/add-rate-limiting.md. Redis prefix: deckhearth:scan.
  • .github/workflows/ci.yml — add a new blocking forbidden-client-side-llm-keys job (~30s grep gate, same shape as forbidden-cors-headers):
    • Fail if apiKey: appears under pages/api/config/*.
    • Fail if generativelanguage.googleapis.com or api.openai.com appears outside pages/api/ (client-side LLM URL leakage).
  • .cursor/rules/api-routes.mdc § Rate limiting — document the new scan class (architect may fold into brief or defer to convoy #2; implementer should at minimum add the lib export).

Out of scope

  • /api/scan/identify server route — convoy server-side-scan-pipeline (#2).
  • Deleting lib/ai-ocr.js browser classes (GeminiVisionOCR, AICardOCR, etc.) — deferred to #2 to avoid breaking the live scanner during this security-only window (the client still needs those classes until the server pipeline replaces them).
  • Rotating the key in source control — the key lives in Vercel env only; rotation is operator action (see below).

Roles invoked

  1. role-architect — ratify scan limiter values; confirm grep patterns for the CI gate; single brief decomposition.
  2. role-implementer — one brief, no fan-out.
  3. role-reviewer — post-PR audit (design + a11y skipped per skip:).

Todos

  • Architect: write .convoys/secure-scanner-gemini-key/brief-1-*.md
  • Implementer: delete config endpoint + client auto-load; extend rate-limit lib
  • Implementer: add forbidden-client-side-llm-keys CI job
  • Operator: rotate GEMINI_AI_API_KEY in Google AI Studio + Vercel before merge
  • Reviewer: verify no key material in diff or CI logs

Operator action required

URGENT — rotate before merge:

  1. Google AI Studio — revoke the current GEMINI_AI_API_KEY and issue a new key. The old key has been exposed to every browser session that loaded the scanner page.
  2. Vercel — update the GEMINI_AI_API_KEY env var on Production + Preview to the new value.
  3. Verify — after deploy, confirm GET /api/config/gemini returns 404 and no network tab in the scanner shows key material.

Do not merge this PR until rotation is complete. The CI gate prevents re-introduction of the leak pattern but does not invalidate a key that was already exfiltrated.

Multitask dispatch

Single brief — no implementer fan-out.

Post-PR audit fan-out:

/multitask role-reviewer

Group id: audit-secure-scanner-gemini-key-<pr> (design-system + a11y auditors skipped per skip: flags).

Cross-convoy parallelism (after #1 merges): open three worktrees and /multitask role-implementer on #2 Brief 1 + #5 Brief 1 + #6 (sole brief) — disjoint file sets (migrations / UI strings / scanner polish).

CI impact

Workflow / job Behavior
ci.ymlforbidden-client-side-llm-keys New blocking job. ~30s grep; no npm ci.
ci.yml → lint, vitest, forbidden-endpoints, forbidden-cors-headers Unchanged; must stay green.
preview-smoke.yml Fires (no paths: filter). Scanner page may error until #2; smoke spec does not exercise scanner.
visual-diff.yml Does not fire — API-only + lib/ + CI YAML; !pages/api/** exclusion applies.

Preserve pipeline: skip smoke PR-body directive for iterative pushes.