On 2026-06-12 a researcher opened an issue on a Next.js project with 33.4k stars, explaining that any logged-in user could read any other user's private file content. The issue is still open. It has zero comments.
Twelve weeks later the report became CVE-2026-85693. The interesting part isn't that a project shipped an access-control bug. It's what the database was doing the entire time.
TL;DR
Chatbot UI's retrieval route takes file UUIDs straight from the request body and queries them with a Supabase service-role client, which ignores Row Level Security. RLS on that table was enabled and the policy was correct. One admin client in one route made it irrelevant. As of 2026-09-18 the bug is unpatched, and because the advisory is an unreviewed GHSA it never reached OSV, so no dependency scanner will flag it for you.
What the route actually does
The vulnerable file is app/api/retrieval/retrieve/route.ts. Here is the shape of it, trimmed to the parts that matter:
const { userInput, fileIds, embeddingsProvider, sourceCount } = json
const uniqueFileIds = [...new Set(fileIds)]
const supabaseAdmin = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const profile = await getServerProfile()
// ...profile is used to pick an OpenAI or Azure API key, and for nothing else
await supabaseAdmin.rpc("match_file_items_openai", {
query_embedding: openaiEmbedding,
match_count: sourceCount,
file_ids: uniqueFileIds
})
Read the path fileIds takes. It arrives in the POST body, gets deduplicated, and goes into the SQL predicate. Nothing in between asks whether the caller owns those files.
getServerProfile() looks reassuring and isn't. It proves you're logged in, then its result is used to choose which API key to embed with. It's authentication standing exactly where an authorization check should be.
The Postgres function doesn't save it either. match_file_items_openai filters on where (file_id = ANY(file_ids)) with no user_id predicate, and it isn't declared security definer.
RLS was on. That's the part worth sitting with.
The obvious assumption is that this project forgot to turn on Row Level Security. It didn't.
Here's the migration that creates the table the leak reads from:
create table file_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
sharing TEXT NOT NULL DEFAULT 'private',
content TEXT NOT NULL,
...
);
ALTER TABLE file_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow full access to own file items"
ON file_items
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
The ownership column exists. RLS is enabled. The policy is the textbook one. Rows default to private.
So every piece of the defense was built, correctly, and then one line in one route opted out of all of it. That's a different bug from the one people usually write about, and it's a more uncomfortable one, because "did you remember to turn on RLS?" is a question you can answer yes to and still be exposed.
A service-role client doesn't weaken your RLS policies. It skips them. Supabase's docs put it plainly: the service_role key carries the Postgres BYPASSRLS attribute. The moment you construct a client with it, every policy you wrote stops running for that query. Whatever those policies were enforcing is now your job, in application code, on every single query.
Why no scanner is going to tell you
This is the part we found most surprising, and it's checkable in one command.
The advisory, GHSA-258v-w87q-m5c9, is marked unreviewed, and unreviewed GitHub advisories aren't exported to OSV. Ask osv.dev about it and you get nothing:
$ curl -s https://api.osv.dev/v1/vulns/CVE-2026-85693
{"code":5,"message":"Vulnerability not found"}
$ curl -s https://api.osv.dev/v1/vulns/GHSA-258v-w87q-m5c9
{"code":5,"message":"Vulnerability not found"}
There's a second reason it can't reach you. Chatbot UI is an application you clone and deploy, not a package you install, so there's no dependency entry in anyone's lockfile to match against. Between the two, the normal safety net simply isn't under this one. npm audit stays quiet. Dependabot stays quiet. If you deployed this, the only way you find out is by reading about it.
It is still live
Not "awaiting a release." Unfixed.
The CVE names commit 81328b6 as the last affected version. On 2026-09-18 that is also the tip of the default branch:
$ git ls-remote https://github.com/mckaywrigley/chatbot-ui.git HEAD refs/heads/main
81328b61d2a4ab597a7a057be70e785cf756d9f8 HEAD
81328b61d2a4ab597a7a057be70e785cf756d9f8 refs/heads/main
No commit of any kind has landed since the commit the advisory points at. The repository isn't archived, and it has no security policy, which is presumably why the reporter filed a public issue rather than a private report. It also has 9.4k forks, and every one of them carries this route.
If you cloned or forked Chatbot UI and put it somewhere real, treat the file-retrieval feature as reading across user boundaries until you've patched it yourself. There's no upstream fix to pull.
The five-minute check on your own app
You almost certainly aren't running this project. The pattern is the part that travels.
Find every server-side use of the admin key. grep -rn "SUPABASE_SERVICE_ROLE_KEY" . across your app. Each hit is a place where none of your RLS policies run.
For each one, trace where the record ID comes from. If it arrives in the request body, a query string, or a URL parameter, keep going. If it's derived from the session, you're fine.
Look for the comparison. Somewhere before the query there must be a check that the record belongs to the caller. Not a check that the caller is logged in. A check that this row is theirs. If you can't point at that line, you have this bug.
Prefer the request-scoped client. If a route doesn't genuinely need admin powers, build the client from the user's session instead and let RLS do the work it was already configured to do. That's the first fix the reporter suggested, and it's the one that fails safe.
What this does and doesn't say about AI-built apps
It would be tidy to call this a story about AI-generated code. It isn't, and the dates make that clear: the migrations are from early 2024 and this is a hand-written project by a named developer. Nobody generated it.
The reason it matters to anyone auditing an app they didn't fully write is narrower, and more useful. The service-role key is the standard escape hatch when RLS blocks a server-side query. It's the thing you reach for when a query returns an empty array and you need it to return rows. Reaching for it is easy, and the ownership check it silently replaced is invisible, because nothing errors and nothing warns you.
An experienced developer did that here, in public, in a project 33.4k people starred. Generated code inherits the same trap, and inherits it without anyone having weighed the tradeoff at all.
If you want the mechanics of which client runs which policy, the Supabase request path diagram walks through the three states a query can be in.
What is CVE-2026-85693?
An authorization bypass (CWE-639) in the open-source project Chatbot UI, published 2026-09-04. The retrieval API accepts file UUIDs from the request body and queries them with a Supabase service-role client, which ignores Row Level Security. Any logged-in user can read indexed content from another user's private files.
Is CVE-2026-85693 patched?
No. As of 2026-09-18 the HEAD of the project's main branch is the same commit the CVE names as the last affected version, so no fix has been committed. The underlying bug report has been open since 2026-06-12 with no maintainer reply.
Does the service role key bypass Row Level Security?
Yes, by design. Supabase's docs are explicit that the service_role key carries the Postgres BYPASSRLS attribute. That is the whole point of it. The danger is that bypassing RLS also removes the ownership check RLS was performing for you, and nothing reminds you to write that check by hand.
Will npm audit or Dependabot warn me about this?
No. The advisory is an unreviewed GHSA, and unreviewed advisories are not exported to OSV. Querying osv.dev for either CVE-2026-85693 or GHSA-258v-w87q-m5c9 returns 'Vulnerability not found'. Chatbot UI is also an application repository rather than a published npm package, so there is no dependency entry for a scanner to match against.
How do I check my own app for this pattern?
Grep your server code for SUPABASE_SERVICE_ROLE_KEY. For every route that uses it, find where the record ID comes from. If the ID arrives in the request body or a URL parameter and there is no comparison against the logged-in user's ID, that route has the same shape as this CVE.
Check What Your App Exposes
Run a free scan and see whether your routes leak data across user boundaries.