> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfosilvia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 2026 08 19 followup findings

# Security follow-up plan — beyond PR #3340

**Source:** CodeRabbit agent security scan, 2026-08-19, 91 open findings
(`high` 12 / `medium` 54 / `low` 25) against `main`. 73 are externally
reachable; 15 rated trivially exploitable.

This doc records what PR #3340 closes and what it deliberately leaves for
later, so the remaining items don't get lost when the PR merges. It is a plan,
not a fix.

***

## 1. Confirmation: the scan independently validated this PR

The scan ran against `main`, which does not yet contain #3340, so its findings
on the files we changed are a second, independent method landing on the same
targets. All nine are fixed on this branch:

| Finding (on `main`) | Sev | Status in #3340 |
| - | - | - |
| `incoming-call-initial-webhook:154` caller\_id → clerk\_id + email | high | fixed (x-tool-secret + HMAC) |
| `internal/generate-proactive-prompts:122` body clerk\_id as identity | high | route deleted (dead) |
| `sms/delivery-status:235` phone numbers + SMS bodies to anon | med | GET handler deleted |
| `sms/radar-complete:114` unauth SMS from Silvia's Twilio | med | `authorizeInternal` |
| `email/scenario-complete:101` broken auth | med | session-ownership authz |
| `email/scenario-complete:118` decrypted email echoed | med | `sentTo` removed |
| `radar/silvia-feed/count:36` clerk\_id count oracle | low | route deleted |
| `middleware.ts:260` `account_deleted` not enforced | high | **NOT fixed — see §4** |

Two findings the 2026-08-13 audit missed, both verified live and now folded
into this PR's migration set (see §2).

***

## 2. Now also covered by this PR — `20260819_01`

Two DB findings that need no code change (no client path touches either table)
are closed in `supabase/migrations/20260819_01_lockdown_internal_finicity_tables.sql`:

* **`public.internal`** — privilege escalation. `authenticated` INSERT +
  `WITH CHECK (clerk_id = jwt sub)` let any logged-in user add their own
  clerk\_id to the 11-row internal allowlist that `lib/is-internal.ts` reads
  (service role) to gate `quant_analyst` and internal surfaces. Locked to
  service-role only.
* **`public.finicity_users`** — `SELECT USING (true)` on a table storing
  `user_secret`. Any authenticated user could read every tenant's aggregator
  credentials. Client read access removed.

***

## 3. Remaining DB findings — NEXT PR (need code first, then revoke)

These three are the same class as `20260813_03` but each has a live app
write/exec path through the RLS-bound Clerk client, so revoking blind would
break a feature. Each needs the code moved to a validated service-role path
first, then the grant/policy removed.

| Object | Finding | Blocking code change |
| - | - | - |
| `tracked_tokens` | med, CWE-862 — unconditional INSERT/UPDATE on the global crypto price registry; one user poisons every user's valuation | Build the server bootstrap/upsert endpoint (already chipped) that validates symbol/token\_id/price; repoint `process-wallet-holdings.ts`, `fetch-wallet-holdings`, coinbase processors; then drop policies. Migration `20260517_06` deliberately skipped this table for this reason. |
| `crunchbase_companies` | med, CWE-862 — `authenticated` UPDATE `USING/CHECK true`; one user rewrites shared company metadata | `crunchbase/company/{save,db,route}.ts` write via `createClerkSupabaseClientSsr` (authenticated role). Move writes to `getSupabaseAdminClient` + server-side validation, then revoke authenticated INSERT/UPDATE. |
| `increment_institution_priority(inst_id, user_id)` | med, CWE-862 — only SECURITY DEFINER fn granted to `authenticated`; trusts caller-supplied `user_id`, not idempotent | `connect-bank/route.ts:372` calls it via the Clerk client. Switch to the admin client, bind `user_id` to the JWT sub inside the fn, add a per-user/per-institution unique record, then revoke authenticated EXECUTE. `snaptrade/process-connection` already uses service role. |

Also verify the **sibling finicity tables** (`finicity_accounts`,
`finicity_connections`, `finicity_holdings`) for the same `USING (true)`
pattern while here.

***

## 4. The theme this PR does not touch: the LLM as confused deputy

\#3340 fixed *who may call a route*. The largest untouched class is *what the
model may do once inside a route* — code, SQL, HTML, and outbound URLs derived
from model output or prompt-injectable content. This is a distinct project.

| Finding | Sev | Risk |
| - | - | - |
| `claude/tools/executors/execute-python.ts:106` | high | Model-authored Python runs in an interpreter holding reusable API credentials; generated code can print and exfiltrate them. |
| `chat/tools/transaction-sql-tool.ts:351` | high | Model-generated SQL executed with **service-role** privileges behind only a *textual* clerk\_id check; if satisfiable without constraining rows → cross-tenant read of the transaction ledger. Powers chat AND the voice SQL tool. |
| `lib/markdown-renderer.tsx:1179` | high | Model-controlled image `src` with no host allowlist/proxy → indirect prompt injection exfiltrates conversation/tool data into a URL. |
| `hyperframes-view/[token]/composition.html/route.ts:117` | high | LLM-authored HTML served as executable, same-origin, inline-JS-allowed page. Stored XSS on the app origin. |
| `claude/tools/executors/personal-info.ts:55` | med | External Trove text treated as model instructions (indirect injection). |
| `sms/incoming/route.ts:1078` | med | SMS agent exposes the paid `send_email` tool. |

**Recommended:** a dedicated "LLM output containment" PR — deterministic guards
on code execution (no ambient secrets), a row-scoped SQL execution path (not
textual checks), an image/URL egress allowlist, and CSP/sandboxing for
LLM-authored HTML.

***

## 5. Capability abuse — highest-priority single route

* **`incoming-call-webhook-send-email-tool/route.ts:170`** (high, trivial):
  unauthenticated; takes `prompt`, `user_id`, and **recipient `email`** from
  the body, runs a paid model, and sends from `silvia@cfosilvia.com` to an
  arbitrary address. Strictly worse than the `scenario-complete` hole this PR
  spent three review rounds on (that only mailed the user's own address). The
  2026-08-13 sweep missed it because it grepped for service-role DB clients and
  this route touches no DB. **Add `x-tool-secret` (sibling voice-tool pattern)
  or delete if the dispatcher supersedes it.** Verify whether the live
  `[name]` dispatcher already covers `send_email` — if so, delete.

***

## 6. Broader IDOR / authz set — NEXT PR

Request-supplied ids trusted against a service-role client without an
ownership predicate:

* `holdings/[id]/route.ts:76` — SnapTrade account row id from the path (same
  branch #3340's chip already flags; confirm the chip's fix covers this line).
* `personal-info/route.ts:129` — every body property except one treated as
  writable.
* `chat/tools/document-reference-tool.ts:32` + `lib/financial-data-cache.ts:529`
  * `documents/process-user-batch/route.ts:390` — document/loan-link ids not
    bound to the caller.
* `email/{welcome,pomp-welcome,mobile}` — authenticated but the recipient
  email is caller-supplied, not the session user's.
* `post-call-webhook:211`, `documents/trigger-processing:34`,
  `email/radar-complete:36` — unauthenticated or observe-only webhook writes.
* `aws/document_backup/lambdas/handler.py:15` (high, trivial) — unauthenticated
  Lambda with bucket-wide S3 write, request fields used as the object key.
* `twilio/events/route.ts:199` — public GET reads private Twilio diagnostics
  via service role (same shape as the `sms/delivery-status` GET this PR
  deleted).

**The structural fix for most of these is the deferred middleware
deny-by-default change** (already chipped): flip `middleware.ts` so `/api/*` is
401 for signed-out callers with an explicit webhook/cron allowlist, and enforce
`account_deleted` there (the `middleware.ts:260` high finding — deletion does
not revoke live Clerk sessions today).

***

## 7. Lower priority — batch these

* **Sensitive logging (≈9 med, mostly trivial):** `extract-holdings:401`,
  `vault/fetch-document-chunks:143`, `webhooks/clerk:462`,
  `incoming-call-webhook-tool/get-account-details:30`,
  `fine-tuning-collector:448`, `lib/auth.ts:11` (NextAuth debug on
  unconditionally). One "scrub PII from logs" sweep.
* **Denial of service (27 findings):** almost all are "no rate limit on an
  expensive/paid route" (AI import, holdings extraction, Plaid Link token,
  fork, Finnhub/EVM lookups). A single edge rate-limit + per-user quota policy
  addresses the bulk; not one-by-one.
* **SSRF:** `ai-import/route.ts:95` — `file_path` from a caller-writable row
  used as a download URL. Fold into the LLM/egress-allowlist work in §4.

## 8. Excluded

* `examples/multiagent/*` (`view_creator.py`, `user_scoped_duckdb_tools.py`) —
  sample code, not shipped. Fix or delete from the repo, but not a production
  risk.

***

## Suggested sequencing

1. **This PR (#3340):** route authz + `20260813_03` + `20260819_01`
   (internal, finicity\_users).
2. **PR: capability + remaining DB** — send-email-tool guard, tracked\_tokens
   endpoint, crunchbase service-role writes, `increment_institution_priority`.
3. **PR: middleware deny-by-default + `account_deleted`** — closes most of §6
   at once.
4. **PR: LLM output containment** (§4) — the largest and most distinct effort.
5. **PR: logging scrub + rate-limit policy** (§7) — mechanical, batchable.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.