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 againstmain, 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:
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.authenticatedINSERT +WITH CHECK (clerk_id = jwt sub)let any logged-in user add their own clerk_id to the 11-row internal allowlist thatlib/is-internal.tsreads (service role) to gatequant_analystand internal surfaces. Locked to service-role only.public.finicity_users—SELECT USING (true)on a table storinguser_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 as20260813_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.
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.
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; takesprompt,user_id, and recipientemailfrom the body, runs a paid model, and sends fromsilvia@cfosilvia.comto an arbitrary address. Strictly worse than thescenario-completehole 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. Addx-tool-secret(sibling voice-tool pattern) or delete if the dispatcher supersedes it. Verify whether the live[name]dispatcher already coverssend_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:529documents/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 thesms/delivery-statusGET this PR deleted).
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_pathfrom 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
- This PR (#3340): route authz +
20260813_03+20260819_01(internal, finicity_users). - PR: capability + remaining DB — send-email-tool guard, tracked_tokens
endpoint, crunchbase service-role writes,
increment_institution_priority. - PR: middleware deny-by-default +
account_deleted— closes most of §6 at once. - PR: LLM output containment (§4) — the largest and most distinct effort.
- PR: logging scrub + rate-limit policy (§7) — mechanical, batchable.