125 lines
4.9 KiB
Markdown
125 lines
4.9 KiB
Markdown
|
|
---
|
|||
|
|
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
|
|||
|
|
53–67). 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
|
|||
|
|
53–67 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.
|