Sample report on a fake app. This app doesn't exist: the engine simulates a leaky Lovable-style app offline, with holes planted on purpose, and scans it exactly like a real one. Run the same thing yourself with npx shippable-dev demo, or scan your own code with npx shippable-dev scan localhost:3000. No sign-up for either.

leaky-demo.exampleSample reportScan your own app

Not Shippable yet.

8 critical or high issues to fix before you ship (plus 11 lower-severity), starting with Stripe secret key exposed in the browser. Each one below comes with proof and a copy-paste fix.

Found 19 leaks. Start with the critical ones. Each has proof and a patch.

Patchy crouching and pointing at the crack a leak comes from
0
critical
0
high
0
medium
0
low

What to fix

Most urgent first · proof and a copy-paste fix for each
Paste it into Lovable, Bolt, Cursor or any AI tool, then re-scan to confirm.
criticalStripe secret key exposed in the browserCredentials exposed to anyone visiting your app

What we found

A Stripe secret key lets anyone create charges, refunds, and read customer/payment data on your account.

Proof

Proof — live evidence
Found stripe-secret (sk_liv…qrst) in publicly served code. sk_liv…qrst

Fix it

Paste the prompt below into your vibecoding tool

Security fix: a secret key (Stripe secret key) is shipped in my app's frontend code, where anyone can read it. Move every use of this key into a server-side function or API route that reads it from an environment variable, remove it from all client-side code, and make sure it is never bundled into the browser again. I will rotate the key myself.

  1. Roll the key now in the Stripe Dashboard → Developers → API keys, and move it to a server-side environment variable.
criticalTable "users" is readable by anyoneData readable by anonymous visitors

What we found

Anyone can download about 1,284 rows from `users`, including email, stripe_customer. Using only the public anon key from your app's page source, an anonymous user can read rows from "users", regardless of whether Row-Level Security is enabled on the table — a table can have RLS turned on and still carry a policy that allows public reads. It exposes email, stripe_customer.

Proof

Proof — live evidence
Read 2 row(s) from "users" anonymously (table holds ~1284 rows total). Exposes: email, stripe_customer. Columns exposed: id, email, full_name, stripe_customer

Fix it

Paste the prompt below into your vibecoding tool

Security fix: the Supabase table "users" is currently readable by anyone on the internet using the public anon key, because Row-Level Security is not restricting it. Enable RLS on public.users and add policies so each user can only read and write their own rows (match auth.uid() to the owner column). Do not allow anonymous or cross-user access to this table.

Supabase SQL editor
-- 1. Turn on Row-Level Security for the exposed table: ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; -- 2. Add a policy that grants ONLY the access you intend. Example: -- (assumes the table has a user_id column identifying the owner) CREATE POLICY "users_owner_can_read" ON public.users FOR SELECT USING (auth.uid() = user_id); -- If this table is meant to be fully public, that is fine — but confirm -- it contains no private data before leaving it open.
criticalTable "orders" is readable by anyoneData readable by anonymous visitors

What we found

Anyone can download about 57 rows from `orders`, including user_email, card_last4. Using only the public anon key from your app's page source, an anonymous user can read rows from "orders", regardless of whether Row-Level Security is enabled on the table — a table can have RLS turned on and still carry a policy that allows public reads. It exposes user_email, card_last4.

Proof

Proof — live evidence
Read 1 row(s) from "orders" anonymously (table holds ~57 rows total). Exposes: user_email, card_last4. Columns exposed: id, user_email, amount_cents, card_last4

Fix it

Paste the prompt below into your vibecoding tool

Security fix: the Supabase table "orders" is currently readable by anyone on the internet using the public anon key, because Row-Level Security is not restricting it. Enable RLS on public.orders and add policies so each user can only read and write their own rows (match auth.uid() to the owner column). Do not allow anonymous or cross-user access to this table.

Supabase SQL editor
-- 1. Turn on Row-Level Security for the exposed table: ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY; -- 2. Add a policy that grants ONLY the access you intend. Example: -- (assumes the table has a user_id column identifying the owner) CREATE POLICY "orders_owner_can_read" ON public.orders FOR SELECT USING (auth.uid() = user_id); -- If this table is meant to be fully public, that is fine — but confirm -- it contains no private data before leaving it open.
criticalTable "invoices" is readable by anyoneData readable by anonymous visitors

What we found

Anyone can download about 312 rows from `invoices`, including customer_email. Using only the public anon key from your app's page source, an anonymous user can read rows from "invoices" — a table name found in your app's code, regardless of whether Row-Level Security is enabled on the table — a table can have RLS turned on and still carry a policy that allows public reads. It exposes customer_email.

Proof

Proof — live evidence
Read 1 row(s) from "invoices" anonymously (table holds ~312 rows total). Exposes: customer_email. Columns exposed: id, customer_email, amount_cents

Fix it

Paste the prompt below into your vibecoding tool

Security fix: the Supabase table "invoices" is currently readable by anyone on the internet using the public anon key, because Row-Level Security is not restricting it. Enable RLS on public.invoices and add policies so each user can only read and write their own rows (match auth.uid() to the owner column). Do not allow anonymous or cross-user access to this table.

Supabase SQL editor
-- 1. Turn on Row-Level Security for the exposed table: ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY; -- 2. Add a policy that grants ONLY the access you intend. Example: -- (assumes the table has a user_id column identifying the owner) CREATE POLICY "invoices_owner_can_read" ON public.invoices FOR SELECT USING (auth.uid() = user_id); -- If this table is meant to be fully public, that is fine — but confirm -- it contains no private data before leaving it open.
criticalYour Realtime Database is readable by anyoneData readable by anonymous visitors

What we found

Anyone can read your entire Firebase Realtime Database, including users, config. This works with no login, because its security rules don't require authentication. We only listed top-level key names to prove this — we did not read any data.

Proof

Proof — live evidence
GET https://demo-firebase-app.firebaseio.com/.json?shallow=true returned 200 with a readable database. Top-level keys: users, config.

Fix it

Paste the prompt below into your vibecoding tool

Security fix: my Firebase Realtime Database has no rules restricting reads, so anyone on the internet can read the entire database with no login. In the Firebase Console, go to Realtime Database → Rules and replace the rules so reads and writes require authentication, then scope each branch to its owner (match auth.uid() to the record's owner field) instead of allowing any signed-in user to read everything.

  1. { "rules": { ".read": "auth != null", ".write": "auth != null" } } // Better: scope each branch to its owner, e.g. for a /users/$uid tree: // "users": { "$uid": { ".read": "auth != null && auth.uid == $uid", ".write": "auth != null && auth.uid == $uid" } }
critical/.env is publicly readableAnyone can download /.env and see 1 secret variable name: DA

What we found

Anyone can download /.env and see 1 secret variable name: DATABASE_URL. This environment file typically holds database URLs, API keys, and other secrets, all readable by anyone on the internet, no login required.

Proof

Proof — live evidence
GET https://leaky-demo.example/.env returned a plaintext env file defining: DATABASE_URL.

Fix it

Paste the prompt below into your vibecoding tool

My deployed app is serving /.env publicly. Make sure this file is never included in the production build/deploy output (exclude it in the build config and/or block dotfiles at the host/CDN level), and confirm the path returns 404 after redeploy. I will rotate any exposed secrets myself.

  1. Remove /.env from your deployed output (exclude it in your build config, and/or block dotfiles at the host/CDN level), then rotate anything it exposed.
highStorage bucket "user-uploads" is publicFiles accessible without authentication

What we found

Anyone can access every file in the "user-uploads" storage bucket. The bucket is marked public, so its files are readable by anyone who knows or guesses an object path.

Proof

Proof — live evidence
Bucket "user-uploads" listed anonymously with public=true.

Fix it

Paste the prompt below into your vibecoding tool

Make the Supabase storage bucket "user-uploads" private and add storage policies so only authorized users can read their own files.

  1. In Supabase → Storage, set the "user-uploads" bucket to Private unless it is truly meant to serve public assets, and add access policies.
highreact-dom 16.0.0 has known vulnerabilitiesA shipped library has a known vulnerability

What we found

Anyone can see your app ships react-dom 16.0.0 in its public JS, which has 1 known vulnerability on record: Demo advisory: a cross-site-scripting issue fixed in a later release.

Proof

Proof — live evidence
Detected react-dom v16.0.0 from a preserved license banner in the shipped JS; OSV lists: GHSA-demo-0001.

Fix it

Paste the prompt below into your vibecoding tool

My app uses react-dom version 16.0.0, which has known security vulnerabilities (GHSA-demo-0001). Upgrade it to 16.0.1 or later and check the changelog/release notes for breaking changes before deploying.

  1. Upgrade react-dom to 16.0.1 or later.
mediumEmail confirmation is disabledAccount signup/auth is weaker than expected

What we found

New accounts are auto-confirmed without verifying the email address. Anyone can sign up as any email they don't own, which enables spoofing and spam.

Proof

Proof — live evidence
auth settings report mailer_autoconfirm = true.

Fix it

Paste the prompt below into your vibecoding tool

Enable email confirmation for signups in my Supabase project so accounts must verify their email before they are active.

  1. In Supabase → Authentication → Providers → Email, turn ON 'Confirm email'.
mediumYour original source code is publicA source map for one of your JS bundles is publicly served.

What we found

A source map for one of your JS bundles is publicly served. Source maps let anyone reconstruct your original, unminified source code — variable names, file structure, comments — straight from the browser dev tools, not just the compiled output.

Proof

Proof — live evidence
GET https://leaky-demo.example/assets/index-demo.js.map returned a source map for https://leaky-demo.example/assets/index-demo.js.

Fix it

Paste the prompt below into your vibecoding tool

My production build is serving JavaScript source maps (.map files) publicly, which exposes my original source code. Disable source maps in production (Vite: build.sourcemap: false, Next.js: productionBrowserSourceMaps: false), or make sure .map files are excluded from the deployed output, then redeploy.

  1. Disable source maps in production builds (Vite: build.sourcemap: false; Next.js: productionBrowserSourceMaps: false — the default), or make sure .map files are excluded from the deployed output. If you need maps for your own error tracking, upload them privately to that provider instead.
lowGoogle API key present in client codeCredentials exposed to anyone visiting your app

What we found

A Google API key is in your bundle. This is normal for Firebase and Maps keys, but only if the key is restricted. An unrestricted key can be abused to run up billed usage.

Proof

Proof — live evidence
Found google-api-key (AIzaSy…0000) in publicly served code.

Fix it

Paste the prompt below into your vibecoding tool

My app ships a Google API key in its frontend. Tell me exactly which HTTP-referrer and API restrictions to set on this key in Google Cloud Console so only my domain and the APIs my app actually uses can call it, and check the code uses no other Google APIs that would break.

  1. In Google Cloud Console → Credentials, add HTTP-referrer and API restrictions to this key so only your domain and intended APIs can use it.
low1 table(s) reachable by anonymous users (no rows returned)Data readable by anonymous visitors

What we found

The anon role can query these tables but they returned no rows — either Row-Level Security is correctly blocking anonymous reads, or the tables are empty. This is NOT proof of exposure, but confirm RLS is enabled and intentional on each: waitlist. If any of these later hold data without RLS, they would become readable.

Proof

Proof — live evidence
Anon SELECT returned 200 with 0 rows for: waitlist.

Fix it

Paste the prompt below into your vibecoding tool

Check these Supabase tables in my app: waitlist. For each one, make sure Row-Level Security is enabled and that no policy lets anonymous (anon) users read private data. Add owner-based policies (auth.uid()) where they're missing, and tell me which tables are intentionally public.

  1. For each table, confirm in Supabase → Authentication → Policies that RLS is ENABLED and no policy grants anonymous read access to private data.
low1 database function(s) callable by anyone with the public keyData readable by anonymous visitors

What we found

Your Supabase project exposes these Postgres functions (RPC endpoints) to anyone holding the public anon key — which includes every visitor to your site, since that key ships in your page source. We have not called any of them (that would not be read-only), but each one should check that the caller is allowed to do whatever it does.

Proof

Proof — live evidence
Function(s) exposed via PostgREST: reset_password.

Fix it

Paste the prompt below into your vibecoding tool

My Supabase project exposes these database functions to anonymous users via PostgREST RPC: reset_password. For each one, check that the function body enforces proper authorization (e.g. an auth.uid() check) before doing anything sensitive, and lock down EXECUTE permissions from anon/public for any that shouldn't be callable by anyone.

  1. For each function, confirm it checks the caller's identity/permissions (e.g. auth.uid()) before doing anything sensitive, or revoke EXECUTE from the anon/authenticated roles if it shouldn't be public.
lowNo Content-Security-Policy headerA recommended security header is missing

What we found

A Content-Security-Policy meaningfully reduces the impact of cross-site scripting (XSS) by controlling what scripts can run. None was set.

Proof

Proof — live evidence
No content-security-policy header in the response.

Fix it

Paste the prompt below into your vibecoding tool

Add a Content-Security-Policy header to every response of my app. First list every external domain the app loads scripts, styles, images, fonts and API calls from, then set a policy that allows only my own origin plus those domains. Start with Content-Security-Policy-Report-Only so nothing breaks, and tell me how to switch it to enforcing once no violations show up.

  1. Add a Content-Security-Policy header. Start in report-only mode, then tighten script-src to your own origin and required CDNs.
lowNo HSTS headerA recommended security header is missing

What we found

Strict-Transport-Security forces browsers to use HTTPS, preventing downgrade and cookie-interception attacks. It was not set.

Proof

Proof — live evidence
No strict-transport-security header in the response.

Fix it

Paste the prompt below into your vibecoding tool

Add the header Strict-Transport-Security: max-age=63072000; includeSubDomains to every response of my app, but only after confirming every page, API and asset (including subdomains) is served over HTTPS.

  1. Add Strict-Transport-Security: max-age=63072000; includeSubDomains once you are confident everything is served over HTTPS.
lowNo clickjacking protectionA recommended security header is missing

What we found

Neither X-Frame-Options nor a Content-Security-Policy frame-ancestors directive is set, so your site can be embedded in an invisible iframe on another site and tricked into accepting clicks meant for something else (clickjacking).

Proof

Proof — live evidence
No x-frame-options header and no frame-ancestors in the CSP.

Fix it

Paste the prompt below into your vibecoding tool

Add clickjacking protection to every response of my app: either the header X-Frame-Options: DENY, or a Content-Security-Policy with frame-ancestors 'none' (use 'self' instead if the app legitimately needs to be embedded on its own domain).

  1. Add X-Frame-Options: DENY (or SAMEORIGIN), or a CSP with frame-ancestors 'none' (or 'self').
lowHTTP does not redirect to HTTPSA recommended security header is missing

What we found

Requesting the plain http:// address of your site does not redirect to https://. Anyone who types the address without 'https', or follows an old http:// link, stays on an unencrypted connection where traffic — including session cookies — can be intercepted.

Proof

Proof — live evidence
GET http://leaky-demo.example/ returned status 200, no redirect to https.

Fix it

Paste the prompt below into your vibecoding tool

Make sure every http:// request to my domain is redirected (301) to the https:// version at the hosting/CDN/reverse-proxy level, before any app code runs, and add a Strict-Transport-Security header once that's confirmed working.

  1. Configure your host/CDN/reverse proxy to redirect every http:// request to the https:// equivalent (301), then add an HSTS header.
lowYour app calls 3 API endpointsYour app's code calls 3 endpoints. We only listed them here

What we found

Your app's code calls 3 endpoints. We only listed them here — we did not call any of them. Every one of these must check who is calling it, since each is reachable by anyone on the internet, not only through your app's own UI. Our AI Red Team (coming soon) will test them for real authorization gaps.

Proof

Proof — live evidence
Endpoints referenced in client code: /api/checkout, /api/admin/export-users, functions/send-receipt.

Fix it

Paste the prompt below into your vibecoding tool

My app's frontend calls these API endpoints: /api/checkout, /api/admin/export-users, functions/send-receipt. For each one, review the server-side handler and confirm it verifies the caller is authenticated and authorized for that specific action/data before doing anything, never trusting a check that only happens in frontend code.

  1. For each endpoint listed, confirm it checks authentication/authorization on the server side — never trust a check that only happens in the browser.
Notes from this scan: Scanned 1 code chunk. Some extra checks were skipped to keep the scan quick and gentle on your site.

Make your AI check before it ships

One paste, and every future session on this project runs the scan
Paste this into your AI tool. It adds a rule to your project (AGENTS.md, plus CLAUDE.md or Cursor rules if you use them), so every future session runs Shippable before calling anything done. Scanning your dev server needs no account.
Prompt for your AI tool
Add this "Before shipping" section to this project's agent instructions so every future session follows it. Put it in AGENTS.md at the project root (create the file if it doesn't exist). If the project also has CLAUDE.md or Cursor rules (.cursor/rules), add it there too. Don't change anything else. ## Before shipping Before telling me this app is ready to ship: 1. Run `npx shippable-dev scan <dev server URL>` (for example localhost:5173). 2. Fix every critical and high finding it reports, then scan again to confirm. 3. After deploying, run `npx shippable-dev scan <live URL>`. 4. Give me the "Full report" file path from the last scan.

Using Cursor, Claude Code or Windsurf? Add Shippable as an MCP server and your agent can run the scan itself: npx -y shippable-dev mcp. Setup for each tool

Four Patchys working as a crew around a server, probing it for weak spots
AI Red Team · coming soon

Want the crew to try the doors next?

AI agents that actively try to break in, scoped from this scan, only on apps you've proven are yours. Coming soon.

One email when early access opens. Nothing else.