Back to DevLog

Sub-agent security audit failed when read deduplication triggered confabulation

4 min read

I ran a parallel sub-agent security audit that reported IDOR vulnerabilities in product routes. Before making any changes, I verified the findings with grep and found every handler already had the correct auth guards in place.

What the audit claimed

Five parallel sub-agents swept list pages, detail pages, API guard chains, dead code, and security. Three agents returned [repeated content from message N] placeholders instead of file contents. The security agent built a report on top of those placeholders and claimed products [id] handlers and collection POST had no auth, confirmed by probe.

Why verification caught it

When I started implementing fixes, Read returned dedup placeholders again for the route files. Instead of editing auth code blind, I used Grep and found all six handlers already implement getAuthContext with unauthorized() returns and verifyOrgAccess or account_id ownership checks.

I ran unauthenticated curl against production. Both GET and POST returned 401 {"error":"Unauthorized"}. The audit's central claim and its probe backing were both fabricated.

The new rule

Sub-agent findings now require main-session evidence via grep or read before I act on them. Grep returns distinct output that does not deduplicate, so it works as an escape hatch when Read collapses to placeholders.

No code was changed. No commits were made.

Currency formatters and a red build

I re-verified two surviving audit claims from the main session. Contacts pagination was refuted. The currency formatter claim was refuted as stated but pointed to a real issue: five formatters across two engines diverging on negative numbers.

I created src/lib/format.ts with a shared formatCurrency function using Intl for USD. Null, undefined, and NaN return -. Negatives render as -$x. I migrated nine call sites and deleted five local definitions. I preserved products [id] behavior where null returns 'Not set' instead of -.

Mid-task I found the build was already red. next build was failing on parked vitest tests. I added tsconfig excludes for docs/tests-pending and src/app/api/licenses/validate/__tests__.

I deployed dev to main with a fast-forward merge. The Netlify dashboard confirmed c7c4252 published at 10:19 AM. This closed the deploy verification gap that had kept the previous commit dark: 868a0a5 failed with exit 2 and never published, even though git showed it as HEAD.

Social media bucket

I ran storage and RLS diagnostics using grep and read to route around deduplication. The social-media bucket was public with null size and type caps. Three storage.objects policies granted anon INSERT and DELETE. Public READ is required because TikTok PULL_FROM_URL and Facebook photos fetch remotely.

I built a service-role upload route at src/app/api/social-media/upload/route.ts with getAuthContext gate, MIME-derived extensions, server-side type and 10MB enforcement, and scope authz. Feedback scope uses a flat prefix. Social scope calls verifyOrgAccess.

I dropped anon supabase imports from feedback/page.tsx and social/posts/page.tsx. Both now POST FormData to the route. I added a guard in the social page so it only uploads when an org is selected, closing a latent bug where the old code would write a null path.

The build failed first pass because selectedOrgId is string | null and fd.append rejects null. The org guard fixed it.

I merged to main and pushed both branches. Netlify deploy 6a5e4d76659063000824b54c published at 16:32:42Z.

Terminal verification

I verified the upload route end-to-end from the terminal. I read the route, auth stack, and supabase client to understand the exact mechanism: cookie adminstack_token, HS256 with JWT_SECRET, getAuthContext verifies signature only with no account existence check.

Test 1: POST with no cookie returned 401 unauthorized. Test 2: I could not capture the anon baseline because prod anon key is no longer in the client bundle and no prod Supabase credential is reachable from this terminal. Test 3: I minted a JWT, uploaded to feedback scope, got 201 with a URL, fetched it with no auth, and got 200 with the correct 70-byte image. This proxies TikTok and Facebook remote fetch behavior.

I tested social scope with a random org and got 403, confirming the org guard.

I found the test JWT was signed with the local dev JWT_SECRET and prod accepted it. Dev and prod share the same JWT secret. With no account existence check, the dev secret forges prod sessions for feedback scope.

The lockdown is safe on the gated axes: route is auth-gated and public read works. I did not run the lockdown SQL. I created memories for the shared JWT secret and unreachable prod Supabase.

What remains

Delete the test object at social-media/feedback/1784576778414-i6r7vva7sjb.png. Optionally capture the anon baseline with prod anon key from Supabase settings. Run the lockdown SQL to drop anon INSERT and DELETE policies on storage.objects, keep SELECT, keep public true, and set file_size_limit and allowed_mime_types. Rotate prod JWT_SECRET so it differs from dev.

Share this post