storage icon indicating copy to clipboard operation
storage copied to clipboard

Failing RLS policy on bucket returns 400 (StorageUnknownError) instead of 403 (Access Denied)

Open tomtitherington opened this issue 9 months ago • 3 comments

Bug report

  • [x] I confirm this is a bug with Supabase, not with my own application.
  • [x] I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

When trying to check the existence of a file within a private storage bucket, if the RLS policy fails, a storage error of type StorageUknownError with HTTP status 400 is returned from the JavaScript client.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Create a private bucket with a simple failing RLS policy.
-- Create the bucket for assets
insert into storage.buckets(id, name, public)
    values ('assets', 'assets', false);

-- Create a policy that will always fail
-- NOTE: Changing false to true here results in a successful response
create policy "Users can never access" on storage.objects
    for select to public using (bucket_id = 'assets' and false);
  1. Apply the migration
  2. Seed it with an asset (optional)
  3. Try to access the bucket by checking the existence of such an asset. For example...
import { createClient } from "@supabase/supabase-js";

const main = async () => {
    // Check for URL, anon key and service role key
    if (!process.env.SUPABASE_URL) {
        console.error("No SUPABASE_URL environment variable found. Exiting...");
        process.exit(1);
    }
    if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
        console.error(
            "No SUPABASE_SERVICE_ROLE_KEY environment variable found. Exiting...",
        );
        process.exit(1);
    }
    if (!process.env.SUPABASE_ANON_KEY) {
        console.error(
            "No SUPABASE_ANON_KEY environment variable found. Exiting...",
        );
        process.exit(1);
    }

    const serviceClient = createClient(
        process.env.SUPABASE_URL,
        process.env.SUPABASE_SERVICE_ROLE_KEY,
    );

    // Create a user
    const DEFAULT_PASSWORD = "Developer123!";
    const adminUserResponse = await serviceClient.auth.admin.createUser({
        email: "[email protected]",
        password: DEFAULT_PASSWORD,
        email_confirm: true,
        user_metadata: {
            first_name: "Bill",
            last_name: "Keys",
        },
    });

    if (!adminUserResponse?.data.user) {
        console.error(
            "No user returned from Supabase signup. Error:",
            adminUserResponse.error,
        );
        console.log("Exiting...");
        process.exit(1);
    }

    const client = createClient(
        process.env.SUPABASE_URL,
        process.env.SUPABASE_ANON_KEY,
    );

    const signIn = async () => {
        const { data, error } = await client.auth.signInWithPassword({
            email: "[email protected]",
            password: "Developer123!",
        });
    };

    await signIn();
    
    const { data: exists, error: assetsError } = await client.storage.from(
        "assets",
    )
        .exists(
            "an-asset.webp",
        );

    console.log("(JWT client) Storage response error: ", assetsError);

};

main();

This should output something like this...

(JWT client) Storage response error:  StorageUnknownError: {}
    at <anonymous> (/Users/tomtitherington/Development/supa-asset-api/supabase/node_modules/.pnpm/@[email protected]/node_modules/@supabase/storage-js/src/lib/fetch.ts:36:12)
    at Generator.next (<anonymous>)
    at fulfilled (/Users/tomtitherington/Development/supa-asset-api/supabase/node_modules/.pnpm/@[email protected]/node_modules/@supabase/storage-js/dist/main/lib/fetch.js:5:58)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
  __isStorageError: true,
  originalError: Response {
    status: 400,
    statusText: 'Bad Request',
    headers: Headers {
      'content-type': 'application/json; charset=utf-8',
      'content-length': '69',
      connection: 'close',
      date: 'Thu, 06 Mar 2025 11:57:09 GMT',
      'access-control-allow-origin': '*',
      'x-kong-upstream-latency': '9',
      'x-kong-proxy-latency': '0',
      via: 'kong/2.8.1'
    },
    body: null,
    bodyUsed: false,
    ok: false,
    redirected: false,
    type: 'basic',
    url: 'http://127.0.0.1:54321/storage/v1/object/assets/5a80860e-d075-4181-96f6-b0c075fcbc15.webp'
  }
}

Expected behavior

I would expect an error of type "AccessDenied" or a HTTP status 403 to be returned as documented here.

System information

  • OS: macOS
  • Version of supabase-js: v2.49.1
  • Version of Node.js: v23.7.0

tomtitherington avatar Mar 06 '25 12:03 tomtitherington

Hey @tomtitherington thanks for reaching out.

Yes, the 400 status code was a bit of a legacy decision to have a single status code for errors, however, we are going to fix it in the next major version of Storage

fenos avatar Mar 12 '25 13:03 fenos

Thanks for getting back @fenos. Is there a roadmap for releases or rough timeline for when this might be coming out?

This is effecting an app we have in production so would be great if you could share some details so we can plan around this.

Thanks!

tomtitherington avatar Mar 13 '25 10:03 tomtitherington

Hey @tomtitherington no worries at all; it is my pleasure 👍

I don't have an exact timeline for this, but it is something we are actively changing for the next major release of Storage. In the meantime, i'd be happy to help handle this error

Could you explain further what the problem you are encountering when trying to handle this error

fenos avatar Mar 13 '25 10:03 fenos

Closing this issue as it will be an upcoming initiative to improve the statusCodes returned

fenos avatar Sep 26 '25 09:09 fenos