Repository navigation
Limit grant-uploads storage access to each file's uploader - #302
Merged
Merged
Conversation
The grant-uploads bucket is private, but its storage.objects policies only checked bucket_id, so anyone with the public anon key could list, download and delete every user's uploaded past grants and reference documents, and upload into the bucket. Replace the six anon/authenticated policies with owner-scoped ones for signed-in users: upload, read and delete only where owner_id is the caller's auth uid. Both uploaders (AI Grant Writer and Project Workspace reference docs) are signed in, and grant-writer already treats owner_id as the owner; the edge functions read with the service role. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPYZwv6X3kmDoJN7g4RaJm
Deploying funder-finder with
|
| Latest commit: |
0ee45e5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b38c8f48.funder-finder.pages.dev |
| Branch Preview URL: | https://claude-grant-uploads-owner-p.funder-finder.pages.dev |
…rs to sign in to upload Review follow-ups on the grant-uploads owner policies: - ai-draft downloads a knowledge-base row's storage_path with the service role, and storage_path is user-writable, so a user could point their own row at another user's file. Only read paths under the caller's reference-docs/<user>/ folder, where Project Workspace uploads them. - Signed-out visitors on the AI Grant Writer could drop files into the bucket; with anon access removed they would get a raw RLS error. Ask them to sign in instead (grant-writer needs a signed-in user anyway). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPYZwv6X3kmDoJN7g4RaJm
…gn-in link Review follow-ups: - The ai-draft path-prefix check could be bypassed with percent-encoded dot segments: storage-js puts the path into the URL unencoded, so reference-docs/<me>/%2e%2e/%2e%2e/<victim>/<file> resolved to another user's file. Now a file is read only if its path has Project Workspace's exact upload shape in the caller's folder and storage.objects records the caller as owner_id, as grant-writer checks. - The Grant Writer shows signed-out visitors a sign-in link in place of the past-grants drop zone (after auth finishes loading, so signed-in users restoring a session never see it), instead of rejecting each file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPYZwv6X3kmDoJN7g4RaJm
…failed ownership lookups Project Workspace takes the extension from the file name's last dot, or the whole name if it has none, so paths like `.../1728.Final Proposal` are legitimate; the 1-10 alphanumeric rule skipped them. Allow anything but a dot, slash, backslash, %, ?, # or control character, none of which the segment can then use to resolve elsewhere. Log the storage.objects lookup error instead of dropping it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPYZwv6X3kmDoJN7g4RaJm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
grant-uploadsis a private bucket, but itsstorage.objectspolicies (created outside migrations) only checkedbucket_id:anonbucket_id = 'grant-uploads'authenticatedbucket_id = 'grant-uploads'The anon key ships in the frontend, so anyone could list, download and delete every user's uploaded past grants (AI Grant Writer) and reference documents (Project Workspace), signed in or not, and upload files into the bucket. I confirmed against live
pg_policiesthat these six are the bucket's only policies and that each one covers onlygrant-uploads.Fix
supabase/migrations/20261013030000_grant_uploads_owner_policies.sqldrops those six policies and adds three forauthenticatedonly:Each one requires
owner_id = auth.uid()::text. Anon gets no access. There's no UPDATE policy: nothing overwrites an upload (upsert: false). Ownership is checked byowner_id, which storage sets from the uploader's JWT andgrant-writeralready checks, not by path. The Grant Writer's paths (<session>/<uuid>.<ext>) carry no user id. Both existing objects haveowner_idset.supabase/functions/ai-draft/index.ts: ai-draft downloads a knowledge-base row'sstorage_pathwith the service role.storage_pathis user-writable, so a user could point their own row at another user's file and get its text into their draft.A path-prefix check alone isn't enough. storage-js puts the path into the request URL unencoded, so
reference-docs/<me>/%2e%2e/%2e%2e/<victim>/<file>resolves as... ai-draft now reads a file only when all of these hold:reference-docs/<uuid>/<uuid>/<digits>.<ext>, where<ext>has no dot, slash, backslash,%,?,#or control character, so nothing can resolve elsewhere;storage.objectsrecords the caller asowner_id, the same check grant-writer does.A failed ownership lookup is logged, and no file is read.
src/pages/GrantWriter.tsx: signed-out visitors see a "Sign in to upload past grants" link in place of the past-grants drop zone. It appears only once auth has loaded, so signed-in users never see it while their session restores.grant-writerneeds a signed-in user anyway, so their uploads were never usable. The upload function keeps a sign-in check as a backstop.Testing
Ran against a local Postgres 16 with
storage.objects,auth.uid(), theanon/authenticatedroles and the current production policies recreated.bug-reports/)The ai-draft path rule accepts real upload paths, including names without a dot such as
1728.Final Proposal. It rejects%2e%2e,.., extra segments, backslashes,?,#and newlines.The migration also applies cleanly twice in a row.
tsc -b, eslint onGrantWriter.tsxandnpm test(70/70) pass.deno checkon ai-draft reports the same 2 errors asmain, both pre-existing./code-review and /security-review were run, then run again on the fixes until both came back with nothing left to fix.
Deploy
ai-draft.The frontend ships with the merge.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UPYZwv6X3kmDoJN7g4RaJm