Storage uploads always fail with RLS violation, before reaching Postgres (any bucket/account) — ticket SU-459997 #49801
Replies: 6 comments
|
The key clue is "BEFORE INSERT trigger never fired across 4 retries" ? because the INSERT itself is not what's failing. Supabase Storage executes You ruled out select policyname, permissive, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by cmd;If there's no create policy "users read own uploads"
on storage.objects for select
to authenticated
using (
bucket_id = 'db'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);Two things worth confirming so we're not chasing the wrong layer:
begin;
select set_config('request.jwt.claims', '{"role":"authenticated","sub":"<THE_TEST_USER_UID>"}', true);
set local role authenticated;
insert into storage.objects (bucket_id, name, owner_id)
values ('db', '<UID>/test.jpg', '<UID>');
rollback;If that fails with 42501, it's purely policy on
The misleading part is that Postgres reports the unqualified relation as |
|
create policy "selfies_verificacao_select_own"
on storage.objects for select
to authenticated
using (bucket_id = 'selfies-verificacao' and
(storage.foldername(name))[1] = auth.uid()::text);
Em ter., 1 de set. de 2026 às 13:03, ARKNET DIGITAL <
***@***.***> escreveu:
… Every storage.objects INSERT via supabase-js (
.storage.from(bucket).upload()) fails with StorageApiError: new row
violates row-level security policy (HTTP 400) — reproduced across:
- Two different buckets (original + one recreated from scratch with
fresh policies)
- Multiple accounts, including a brand-new signup
- Fresh sessions (re-login)
Regular PostgREST calls (RPC, table updates) work fine with the same
sessions. RLS policies confirmed correct via pg_policies (PERMISSIVE,
{authenticated}, correct with_check).
Added a temporary BEFORE INSERT trigger on storage.objects — it never
fired across 4 retries, even though postgres_logs shows the RLS error
text each time. This suggests the block happens inside the Storage
service's own authorization check, before any real INSERT reaches the table.
Project also shows "Exceeding Usage Limits" (Egress at 5.3GB vs the 5GB
Free cap) — wondering if that's connected to Storage writes being rejected
project-wide.
Project ref: uveeqjtufjuiemdmrwbp. Official ticket: SU-459997. Happy to
share more logs/reproduce live.
This isn't an RLS bug — it's the Free-tier quota enforcement. You flagged
it yourself almost in passing: "Exceeding Usage Limits (Egress at 5.3GB vs
the 5GB Free cap)." Supabase throttles/blocks Storage writes project-wide
once a Free project is over its usage cap, and the error it returns is the
same generic "row-level security policy" 403/400 you'd get from an actual
policy failure — which is exactly why your trigger never fired and why it
reproduces across buckets, accounts, and fresh sessions: it's happening
before Postgres ever sees the INSERT. Bump the project over the Egress
limit (upgrade, or wait for the cycle reset) and retest before chasing the
policies further — they're very likely fine as-is.
—
Reply to this email directly, view it on GitHub
<#49801?email_source=notifications&email_token=CDZY3BJMK6TKJTOPTUBTMP35M3XMTA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBSGM3DIMBSUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVRTG633UMVZF6Y3MNFRWW#discussioncomment-18236402>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/CDZY3BJEBPUPMI2QST6FTJT5M3XMTAVCNFSNUABIKJSXA33TNF2G64TZHMZDCNBVHA3TCOJTHNCGS43DOVZXG2LPNY5TCMBXGIZTSMRZUF3AE>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/CDZY3BOVR5HFPLWDTCYMJHL5M3XMTA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBSGM3DIMBSUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVJTG633UMVZF62LPOM>
and Android
<https://github.com/notifications/mobile/android/CDZY3BLKJAWC66QM5O6IHOD5M3XMTA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBSGM3DIMBSUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
vemtreinar — your policy attempt is close. The
Working template (assuming client uploads to -- SELECT: user reads own folder
create policy "selfies_select_own" on storage.objects
for select to authenticated
using (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
-- INSERT: user uploads to own folder
create policy "selfies_insert_own" on storage.objects
for insert to authenticated
with check (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
-- UPDATE/DELETE: user manages own files
create policy "selfies_update_own" on storage.objects
for update to authenticated
using (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text)
with check (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
create policy "selfies_delete_own" on storage.objects
for delete to authenticated
using (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);Key fixes vs your version:
To debug: run the failing upload in Supabase SQL Editor with the user's JWT: set role authenticated;
set local request.jwt.claims = '{"sub":"<user-id>","role":"authenticated"}';
insert into storage.objects (bucket_id, name, owner, metadata)
values ('selfies-verificacao', '<user-id>/test.jpg', '<user-id>', '{}');If that fails, the policy is the blocker. If it succeeds, the issue is client-side (wrong path format). Post the exact error from the client upload if this doesn't resolve it — happy to trace further. |
|
eu ja resolvi meu problema , muito obrigado pelo suporte nota:10
Em ter., 1 de set. de 2026 às 17:45, Cenk KURTOĞLU ***@***.***>
escreveu:
… vemtreinar — your policy attempt is close. The storage.foldername(name)
approach is the right idea, but two issues:
1.
*foldername(name) on a flat upload* (no folder prefix) returns the
filename itself, not a folder. Your policy bucket_id =
'selfies-verificacao' AND foldername(name) = auth.uid()::text will
only match if the object is uploaded to a *folder named exactly the
user's UUID*. If the client uploads directly to
selfies-verificacao/<uuid>/... it works; if flat, it fails.
2.
*The INSERT policy must use WITH CHECK*, not USING. Storage uses INSERT
for uploads. Your posted policy only has USING.
*Working template* (assuming client uploads to
selfies-verificacao/<user_id>/):
-- SELECT: user reads own folder
create policy "selfies_select_own" on storage.objects
for select to authenticated
using (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
-- INSERT: user uploads to own folder
create policy "selfies_insert_own" on storage.objects
for insert to authenticated
with check (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
-- UPDATE/DELETE: user manages own files
create policy "selfies_update_own" on storage.objects
for update to authenticated
using (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text)
with check (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
create policy "selfies_delete_own" on storage.objects
for delete to authenticated
using (bucket_id = 'selfies-verificacao'
and (storage.foldername(name))[1] = auth.uid()::text);
*Key fixes vs your version:*
- (storage.foldername(name))[1] extracts the *first* path segment (the
folder)
- WITH CHECK on INSERT/UPDATE (not USING)
- Separate policies per operation (cleaner debugging)
*To debug*: run the failing upload in Supabase SQL Editor with the user's
JWT:
set role authenticated;set local request.jwt.claims = '{"sub":"<user-id>","role":"authenticated"}';insert into storage.objects (bucket_id, name, owner, metadata) values ('selfies-verificacao', '<user-id>/test.jpg', '<user-id>', '{}');
If that fails, the policy is the blocker. If it succeeds, the issue is
client-side (wrong path format).
Post the exact error from the client upload if this doesn't resolve it —
happy to trace further.
—
Reply to this email directly, view it on GitHub
<#49801?email_source=notifications&email_token=CDZY3BLNIDA55G7FRVM5C735M4YONA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBSGQYDAMZTUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVRTG633UMVZF6Y3MNFRWW#discussioncomment-18240033>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/CDZY3BKIDJGPL6GUKALMX3L5M4YONAVCNFSNUABIKJSXA33TNF2G64TZHMZDCNBVHA3TCOJTHNCGS43DOVZXG2LPNY5TCMBXGIZTSMRZUF3AE>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/CDZY3BJKVEXR42NSLBFWQV35M4YONA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBSGQYDAMZTUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVJTG633UMVZF62LPOM>
and Android
<https://github.com/notifications/mobile/android/CDZY3BJN6LWIX47TTSKVMUD5M4YONA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBSGQYDAMZTUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
This is a known Supabase platform issue (ticket SU-459997) — the Storage API rejects uploads at the PostgREST layer before RLS even evaluates, for certain JWT configurations (especially ES256/JWTs from external auth providers). Workarounds:
Check your JWT algorithm: -- Decode your JWT at jwt.io — look at "alg" header
-- If "ES256" → this is the known bugStatus: Supabase team is aware. Track SU-459997 for fix. In the meantime, server-side upload with service_role is the reliable path. Free audit script: https://github.com/cekuu35/supabase-rls-leak-demo |
|
One caution on the workaround list, because the bucket in this thread holds verification selfies. Making the bucket public is not a temporary workaround. A public bucket serves every object to anyone with the URL: no session, no token, no expiry. Object names are guessable more often than people expect, and once the bucket has been public the URLs are cacheable and may already be indexed. For identity-document images that is a personal-data exposure, and under the GDPR it is the kind of event that can trigger the Article 33 notification duty rather than a config change you quietly revert. The safe version of the same idea keeps the bucket private and moves the upload to the server: // server route or edge function, never the browser
const admin = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
const { error } = await admin.storage.from('selfies-verificacao')
.upload(`${user.id}/${filename}`, file, { contentType: file.type });and hands back a short-lived link when the file is read: const { data } = await admin.storage.from('selfies-verificacao')
.createSignedUrl(`${user.id}/${filename}`, 60);That gets uploads working immediately, exactly like the public-bucket workaround, without the exposure. On the cause itself, the Worth checking both halves exist, since a policy for select policyname, cmd, roles, qual as using_clause, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects';And two details that catch people with the folder-prefix pattern:
If the SELECT policy turns out to be the cause, the trigger evidence and the platform-issue theory both resolve without needing a ticket. |
Uh oh!
There was an error while loading. Please reload this page.
Every
storage.objectsINSERT via supabase-js (.storage.from(bucket).upload()) fails withStorageApiError: new row violates row-level security policy(HTTP 400) — reproduced across:Regular PostgREST calls (RPC, table updates) work fine with the same sessions. RLS policies confirmed correct via
pg_policies(PERMISSIVE,{authenticated}, correctwith_check).Added a temporary
BEFORE INSERTtrigger onstorage.objects— it never fired across 4 retries, even thoughpostgres_logsshows the RLS error text each time. This suggests the block happens inside the Storage service's own authorization check, before any real INSERT reaches the table.Project also shows "Exceeding Usage Limits" (Egress at 5.3GB vs the 5GB Free cap) — wondering if that's connected to Storage writes being rejected project-wide.
Project ref:
uveeqjtufjuiemdmrwbp. Official ticket: SU-459997. Happy to share more logs/reproduce live.All reactions