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

124 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: secure-scanner-gemini-key
classification: server-only
success_metric: |
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.
skip:
- ia
- ux
- visual
- a11y
- design
status: in-progress
created: 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`
- [x] Implementer: delete config endpoint + client auto-load; extend rate-limit lib
- [x] Implementer: add `forbidden-client-side-llm-keys` CI job
- [x] 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.yml``forbidden-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.