Blog · Infrastructure · 2026-08-27

Exposed storage buckets and secrets: the shadow AI crisis

Security researchers scanning thousands of publicly reachable, AI-generated apps keep landing on the same two findings, over and over: storage buckets anyone can browse, and API keys sitting in plain sight in the JavaScript everyone's browser already downloaded.

Fine to expose

A storage bucket's name or URL, visible in your client-side code. A public/publishable API key meant to be client-side (Stripe publishable key, Supabase anon key, Firebase client config).

Not fine to expose

A bucket with public listing enabled and no access control behind it. Any secret, admin, or service-role key — the ones meant to run only on your server — reachable in a browser-downloaded bundle.

What large-scale scans keep finding

One widely cited scan of over five thousand publicly available AI-generated applications found more than two thousand high-impact vulnerabilities, over four hundred exposed secrets — API keys and access tokens sitting directly in reachable code — and over a hundred and seventy instances of exposed personal data, including medical records and bank account numbers. Separate research specifically tracking "shadow AI" — apps and tools built and deployed outside a company's normal security review — identified hundreds of thousands of publicly accessible assets from AI coding platforms, with a meaningful share of those exposing genuinely sensitive corporate data.

These aren't obscure, hard-to-find issues requiring a sophisticated attacker. A misconfigured Google Cloud Storage bucket behind one AI-built photo and video app exposed roughly one and a half million images and hundreds of thousands of videos to anyone who requested the bucket's contents — no credentials needed, because the bucket itself never required any.

Why this keeps happening in AI-scaffolded apps specifically

Getting file uploads working is a two-step process, and AI coding assistants are extremely good at the first step and inconsistent about the second. Step one: create a bucket, wire up an upload flow, confirm files actually land in storage — done, demo works. Step two: configure the bucket so only intended parties can read what's in it, usually through signed URLs or a bucket policy scoped to specific access patterns. That second step is a distinct, deliberate action that has to be requested explicitly — it doesn't happen as a side effect of "add file uploads to my app," and a fast-moving build frequently ships without it.

The secrets side follows a similar pattern for a different reason: a developer copies a working code snippet — sometimes from an AI assistant's own example output — that includes a key directly in the source rather than reading it from an environment variable, because that's the fastest way to see the feature work locally. It works, it ships, and the key rides along in whatever gets bundled and sent to every visitor's browser.

How to check your own app

Search your production JavaScript bundles for any storage bucket URL your app references, then request that bucket's listing endpoint directly (for S3-compatible storage, this is typically the bucket root URL with no path). If it returns a file listing instead of an access-denied response, it's publicly browsable.

Separately, search those same bundles for the shape of a hardcoded secret — a variable assignment near words like key, secret, or token followed by a long alphanumeric string — and for raw database connection strings, which often embed a username and password directly in the URL:

# Grep a built bundle for common connection-string and key shapes
grep -rEo '(postgres|mysql|mongodb)(\+srv)?://[^"'"'"' ]+' .next/static/
grep -rEo '(sk_live|sk_test|service_role|SECRET_KEY)[A-Za-z0-9_.=:-]{10,}' .next/static/

The fix

Make every storage bucket private by default, and serve individual files through short-lived signed URLs generated server-side rather than a permanently public bucket policy — the bucket name being visible stops mattering once nothing can be done with just the name:

// Server-side: generate a short-lived, scoped URL instead of a public bucket
const signedUrl = await storage
  .from("user-uploads")
  .createSignedUrl(filePath, 60 * 5); // expires in 5 minutes

For secrets, keep anything that isn't explicitly designed to be public — service-role keys, admin credentials, database connection strings, model provider API keys — in server-side environment variables only, never referenced in a client component or committed to the repository. If a secret key has ever been exposed, even briefly, rotate it; a key you've seen in your own bundle is a key you have to assume someone else has seen too.

Are your buckets and keys actually private?

StackSecured's Storage Bucket Auditor checks whether any bucket referenced in your client JS is publicly listable, and the Client-Side Secret Filter scans your bundles for hardcoded secrets and raw connection strings — both included in every free scan.

Run a free scan

Common questions

What does "shadow AI" actually mean?

+

It's the term security researchers use for AI-built apps and tools spun up outside a company's normal review process — a founder prototyping on a weekend, a team member trying an idea in Bolt or Lovable, an internal tool nobody formally approved. They ship fast, work, and often get forgotten about while still being publicly reachable — which is exactly the profile large-scale scans keep finding wide open.

If my storage bucket URL is public in my JavaScript bundle anyway, isn't the bucket name already exposed no matter what I do?

+

The bucket name being visible isn't the vulnerability — what the bucket allows anyone to do once they know its name is. A private bucket with public listing disabled and access controlled through signed, time-limited URLs is completely fine to have its name visible in your JS bundle; there's nothing a stranger can do with the name alone. A publicly listable bucket with no access controls turns that same visible name into every file anyone has ever uploaded, browsable by anyone.

Is rotating an exposed key enough, or do I need to do more?

+

Rotating the key stops future misuse but doesn't undo anything that already happened while it was exposed. Check the provider's access logs (most cloud platforms and Stripe, Supabase, and similar services keep them) for any usage from IPs or patterns you don't recognize during the exposure window, and treat any data the key could access as potentially viewed, not just potentially viewable.

We're pre-launch with almost no users — does this actually matter yet?

+

It matters more, not less. Automated scanners don't check whether you have users before finding a publicly listable bucket or a hardcoded key in a JS bundle — they scan indiscriminately across the open internet, and a fresh, unindexed app is not the same as an unreachable one. The exposure window that matters is between 'the code is live' and 'someone finds it,' which for automated scanning tools can be hours, not months.

More reading: Exposed API keys in AI-built apps · Broken access control · Full vibe-coding security checklist