Sample report
What a survey report looks like
This is a complete free survey of demo-shop.example, a made-up shop built with Lovable and left with the problems we see most often. Your own report looks the same: the finding titles show at once, and the details unlock with your email.
- Kind
- Free survey
- Built with
- Lovable
- Address surveyed
- https://demo-shop.example/
Survey grade
19 / 100
Like the confidence stars on a chart: how far you can trust these waters, from what a browser can see.
About this sample
demo-shop.example is not a real site. These are the findings our scanner reports for a test copy of a typical Lovable app, word for word. The keys in it are fake, and only their first and last four characters are ever shown, as in every report.
Chart of findings
Legend
- Critical
- 2
- High
- 0
- Medium
- 3
- Low
- 2
- Info
- 7
We charted 14 findings. They are listed below, most serious first.
What we found
Open any finding to read what we saw, why it matters and how to fix it.
Critical (2)
-
A Stripe live secret key is in your public code Critical Keys and secrets
- What we saw
- Your site sends a file to every visitor containing what looks like a Stripe live secret key (sk_l********e5Gh). Anyone can see it by viewing your site's source.
- Why it matters
- Anyone who copies this key can act as your business in Stripe: read customer and payment details, issue refunds and change your account.
- How to fix it
- First, replace the key. In the Stripe Dashboard, open Developers, then API keys, and roll the key. Removing it from your code alone is not enough, because copies of the old files stay in browser caches, CDNs and web archives. Then move it out of the browser code. If your project uses Supabase, you can store the key as a Supabase secret and call the service from a Supabase Edge Function, so the key never reaches the browser. Otherwise, keep it in a server-side function and read it from an environment variable.
- Technical detail
- Type: Stripe live secret key Masked: sk_l********e5Gh Found in: - https://demo-shop.example/assets/index-Bq7xT2mK.js Key (masked): sk_l********e5Gh Seen at: https://demo-shop.example/assets/index-Bq7xT2mK.js
-
A Supabase service_role key is in your public code Critical Keys and secrets
- What we saw
- Your site sends a file to every visitor containing a Supabase key (eyJh********7Vb3) with the service_role role. Unlike the anon key, which is meant to be public, the service_role key is for servers only.
- Why it matters
- This key bypasses Row Level Security. Anyone who copies it can read, change or delete every row in your Supabase database, including your users' data.
- How to fix it
- First, replace the key. In the Supabase dashboard, open Project Settings, then API Keys, and rotate the key (for the legacy service_role key this means rotating the JWT secret, which also changes your anon key). Then check your tables for changes you did not make. Removing it from your code alone is not enough, because copies of the old files stay in browser caches, CDNs and web archives. Then move it out of the browser code. If your project uses Supabase, you can store the key as a Supabase secret and call the service from a Supabase Edge Function, so the key never reaches the browser. Otherwise, keep it in a server-side function and read it from an environment variable.
- Technical detail
- Type: Supabase service_role key Masked: eyJh********7Vb3 Supabase project: fakeprojectrefabcdxy Found in: - https://demo-shop.example/assets/index-Bq7xT2mK.js Key (masked): eyJh********7Vb3 Seen at: https://demo-shop.example/assets/index-Bq7xT2mK.js
Medium (3)
-
No Content-Security-Policy Medium Security headers
- What we saw
- Your homepage does not send a Content-Security-Policy header or meta tag, so browsers will run any script that ends up in your pages.
- Why it matters
- A Content-Security-Policy is the main safety net if an attacker manages to inject a script, for example through a comment, a profile field or a compromised third-party library. Without one, that script can read what your users see and act as them.
- How to fix it
- Add a policy that lists where your scripts, styles and API calls may come from, for example: Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'. Add the services your app really uses (Supabase, Stripe, analytics) and try it first as Content-Security-Policy-Report-Only so nothing breaks. If your host does not let you set response headers, you can add them by putting the site behind a CDN such as Cloudflare, or by hosting the built app on Netlify or Vercel, where headers are set in a config file (_headers or vercel.json). On a static host, a <meta http-equiv="Content-Security-Policy"> tag in index.html also works for everything except frame-ancestors.
- Technical detail
- No Content-Security-Policy header and no <meta http-equiv="Content-Security-Policy"> on https://demo-shop.example/ Seen at: https://demo-shop.example/
-
No Strict-Transport-Security header Medium Security headers
- What we saw
- Your site works over HTTPS, but it does not send a Strict-Transport-Security (HSTS) header telling browsers to always use HTTPS.
- Why it matters
- Without it, a visitor who types your address or follows an old http:// link makes a first unencrypted request, which someone on the same network (for example public Wi-Fi) can intercept and redirect.
- How to fix it
- Send Strict-Transport-Security: max-age=31536000 (add includeSubDomains once every subdomain works over HTTPS). If your host does not let you set response headers, you can add them by putting the site behind a CDN such as Cloudflare, or by hosting the built app on Netlify or Vercel, where headers are set in a config file (_headers or vercel.json).
- Technical detail
- No Strict-Transport-Security header on https://demo-shop.example/ Seen at: https://demo-shop.example/
-
Your site publishes source maps Medium Exposed files
- What we saw
- We downloaded a source map from your site. It includes the original source code of 4 of your files, so anyone can read your code as it was written, not only the compressed version browsers run.
- Why it matters
- Your source code is not a secret in itself, but readable code makes it much easier for someone to find your API endpoints, admin routes, feature flags and any mistakes in your logic. Anything left in the code, such as comments, internal notes or test values, can be read too.
- How to fix it
- Turn off public source maps in your production build. In a Vite project, set build.sourcemap to false in vite.config (it is off by default, so look for sourcemap: true). If you need them for error tracking, upload them to your error tracker (for example Sentry) during the build and delete the .map files before deploying.
- Technical detail
- Source maps anyone can download: - https://demo-shop.example/assets/index-Bq7xT2mK.js.map (from index-Bq7xT2mK.js): 4 sources, source code included Seen at: https://demo-shop.example/assets/index-Bq7xT2mK.js.map
Low (2)
-
X-Content-Type-Options is not set to nosniff Low Security headers
- What we saw
- Your homepage does not send an X-Content-Type-Options header.
- Why it matters
- Browsers may guess the type of a file instead of trusting your server. If users can upload files, a file could then be run as a script or stylesheet.
- How to fix it
- Send X-Content-Type-Options: nosniff on every response. If your host does not let you set response headers, you can add them by putting the site behind a CDN such as Cloudflare, or by hosting the built app on Netlify or Vercel, where headers are set in a config file (_headers or vercel.json).
- Technical detail
- No X-Content-Type-Options header Seen at: https://demo-shop.example/
-
Other websites can show your site in a frame Low Security headers
- What we saw
- Your homepage sends neither X-Frame-Options nor a Content-Security-Policy frame-ancestors directive, so any website can load it inside an invisible frame.
- Why it matters
- This allows clickjacking: another site overlays your app and tricks a signed-in user into clicking buttons they cannot see, such as changing settings or confirming a payment.
- How to fix it
- Send Content-Security-Policy: frame-ancestors 'self' (or X-Frame-Options: SAMEORIGIN for older browsers). This must be a response header; browsers ignore frame-ancestors in a meta tag. If your host does not let you set response headers, you can add them by putting the site behind a CDN such as Cloudflare, or by hosting the built app on Netlify or Vercel, where headers are set in a config file (_headers or vercel.json).
- Technical detail
- No X-Frame-Options DENY or SAMEORIGIN, and no frame-ancestors directive in a Content-Security-Policy header Seen at: https://demo-shop.example/
Info (7)
-
No Referrer-Policy header Info Security headers
- What we saw
- Your homepage does not set a Referrer-Policy. Modern browsers then use strict-origin-when-cross-origin, which only shares your domain with other sites, so this is a small gap.
- Why it matters
- Setting it explicitly keeps the same behaviour in older browsers, where full page addresses (which can include private links or tokens) may be shared with other sites.
- How to fix it
- Send Referrer-Policy: strict-origin-when-cross-origin (or no-referrer if you do not need referrer information). If your host does not let you set response headers, you can add them by putting the site behind a CDN such as Cloudflare, or by hosting the built app on Netlify or Vercel, where headers are set in a config file (_headers or vercel.json).
- Technical detail
- No Referrer-Policy header or <meta name="referrer"> Seen at: https://demo-shop.example/
-
No Permissions-Policy header Info Security headers
- What we saw
- Your homepage does not send a Permissions-Policy header, which lets you switch off browser features your app does not use, such as the camera, microphone or location.
- Why it matters
- This is an extra layer rather than a gap: it limits what an injected script or embedded third party could ask the browser for.
- How to fix it
- Send a policy that turns off what you do not use, for example Permissions-Policy: camera=(), microphone=(), geolocation=(). If your host does not let you set response headers, you can add them by putting the site behind a CDN such as Cloudflare, or by hosting the built app on Netlify or Vercel, where headers are set in a config file (_headers or vercel.json).
- Technical detail
- No Permissions-Policy header Seen at: https://demo-shop.example/
-
Your app talks to Supabase with its public key Info Exposed files
- What we saw
- Your site's code contains your Supabase project address and its public (anon or publishable) key. This is normal: Supabase apps work this way, and this key is designed to be public.
- Why it matters
- Anyone can take this key and query your database directly, without going through your app's screens. Tables without Row Level Security (RLS), or with policies that are too open, can then be read or changed by anyone. A passive scan cannot see your policies.
- How to fix it
- In the Supabase dashboard, check the Security Advisor and make sure Row Level Security is enabled on every table in the public schema, with policies that only let each user reach their own rows. Soon, a verified scan will be able to check whether your tables are readable without logging in.
- Technical detail
- Supabase project: fakeprojectrefabcdxy Public key: eyJh********7Vb3 Seen at: https://demo-shop.example/assets/index-Bq7xT2mK.js
-
No SPF record for demo-shop.example Info Email
- What we saw
- demo-shop.example has no SPF record. It does not seem to handle email, so this matters less, but a record saying it sends none still helps receivers reject forgeries.
- Why it matters
- Anyone can send email that claims to come from @demo-shop.example, for example fake invoices or password reset emails to your customers, and receiving mail servers are not told to reject it. It can also hurt the delivery of your real email.
- How to fix it
- Publish one TXT record at demo-shop.example that lists the services allowed to send your email and ends in ~all or -all, for example v=spf1 include:_spf.google.com ~all (your email provider's help pages give the exact include). If the domain sends no email at all, publish v=spf1 -all.
- Technical detail
- No TXT record starting with v=spf1 at demo-shop.example
-
No DMARC policy for demo-shop.example Info Email
- What we saw
- There is no DMARC record at _dmarc.demo-shop.example, so receiving mail servers have no instruction for email that fails SPF or DKIM checks, and you get no reports about who sends email as demo-shop.example.
- Why it matters
- Anyone can send email that claims to come from @demo-shop.example, for example fake invoices or password reset emails to your customers, and receiving mail servers are not told to reject it. It can also hurt the delivery of your real email.
- How to fix it
- Publish a TXT record at _dmarc.demo-shop.example such as v=DMARC1; p=none; rua=mailto:[email protected] to start receiving reports, then move to p=quarantine and finally p=reject once the reports show only your own services send your email.
- Technical detail
- No TXT record at _dmarc.demo-shop.example
-
No CAA record Info Domain and DNS
- What we saw
- Neither demo-shop.example nor its parent domains publish a CAA record, so any certificate authority may issue certificates for your domain.
- Why it matters
- This is a small extra safeguard rather than a gap. A CAA record limits which authorities can issue certificates, which makes a wrongly issued certificate less likely.
- How to fix it
- Add a CAA record at demo-shop.example naming the authority your host uses, for example 0 issue "letsencrypt.org". Check with your host first: if it switches authority, certificates will stop renewing.
- Technical detail
- No CAA records on demo-shop.example
-
Your domain does not use DNSSEC Info Domain and DNS
- What we saw
- A validating DNS resolver did not mark the answers for demo-shop.example as signed, so your domain does not use DNSSEC. Most websites do not use it either.
- Why it matters
- DNSSEC protects visitors against forged DNS answers on hostile networks. Without it you rely on HTTPS alone to catch that, which it usually does.
- How to fix it
- If your registrar or DNS provider offers DNSSEC (often a single switch, for example in Cloudflare's DNS settings), turn it on and let it publish the DS record at your registrar.
- Technical detail
- DNS-over-HTTPS query for demo-shop.example (A) answered without the AD (authenticated data) flag
Survey your own app
Free, with nothing to install. We only read what your homepage already sends to every visitor.