PL-030mediumLive posture

No Content-Security-Policy

Your page sends no Content-Security-Policy, so any script that gets onto it can run and send data anywhere.

Severity
Medium
Who can exploit it
Anyone who gets a script onto your page
Found by
PatchLabs Live posture: your home page’s response headers
Lab proof
Not lab-proven: read from your home page’s response

What it is

A Content-Security-Policy (CSP) is a response header that lists where your page may load scripts, styles, images and connections from. The browser blocks everything else. PL-030 means your home page sends no CSP at all.

A CSP doesn’t fix a bug; it limits the damage of one. If user content reaches the page as HTML (PL-024), or a third-party script is compromised, a good policy stops the injected script from running or from sending what it reads to another server. That matters more than usual in a Supabase app, because supabase-js keeps the signed-in session in localStorage by default, where any script on the page can read it.

Why AI builders produce it

A CSP has to name every place your app talks to, and that list is specific to each app: your Supabase project, your analytics, your fonts. Generators can’t know it, and a wrong guess breaks the app, so the safe default for a tool is to leave it out.

What we see

A synthetic example for app.example.com, shortened. Not taken from any real site.

curl -sI https://app.example.com (response, shortened)
$ curl -sI https://app.example.com
HTTP/2 200
content-type: text/html; charset=utf-8
# no content-security-policy line: any script may run and connect anywhere

Check it yourself

check-pl-030.sh
curl -sI https://your-app.com | grep -iE 'content-security-policy'
# also check index.html for <meta http-equiv="Content-Security-Policy" ...>

In the browser, open DevTools → Console after you add a policy: every blocked load is listed with the directive that blocked it, which is how you tune it.

The fix

Start in report-only mode. Content-Security-Policy-Report-Only takes the same value, blocks nothing and prints violations in the console. Use the app for a few days, add the sources it genuinely needs, then switch the header name to Content-Security-Policy. A starter policy for a Vite or React app on Supabase, with your project ref in place of YOUR-PROJECT:

starter policy (one line in the real header)
default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob: https://YOUR-PROJECT.supabase.co;
  font-src 'self' data:;
  connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

The wss:// entry is for Realtime; storage downloads come from the same supabase.co host. Add what your app uses on top, for example https://fonts.googleapis.com to style-src and https://fonts.gstatic.com to font-src for Google Fonts, or your analytics host to script-src and connect-src. Avoid 'unsafe-inline' in script-src: it lets injected scripts run, which is what the policy is for.

Vercel

vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Content-Security-Policy-Report-Only", "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://YOUR-PROJECT.supabase.co; font-src 'self' data:; connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" }
      ]
    }
  ]
}

Next.js apps can set the same header from headers() in next.config.js, or per request in middleware when you need a nonce for inline scripts.

Netlify

public/_headers
# public/_headers  (Vite copies public/ into the publish folder)
/*
  Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://YOUR-PROJECT.supabase.co; font-src 'self' data:; connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

The file must end up in your publish directory (dist for Vite). Bolt projects deployed to Netlify use this file too. The same rule can live in netlify.toml under [[headers]] if you prefer one config file.

Cloudflare

For a site proxied through Cloudflare (orange cloud), add a Response Header Transform Rule (Rules → Transform Rules in the dashboard): match all incoming requests to your hostname, then Set static header Content-Security-Policy-Report-Only to:

Content-Security-Policy-Report-Only · value
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://YOUR-PROJECT.supabase.co; font-src 'self' data:; connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

On Cloudflare Pages or Workers static assets, a _headers file in your build output works the same way as on Netlify.

Lovable

Lovable’s documentation (checked 10 Nov 2026) doesn’t describe a way to set your own response headers on Lovable hosting. It does let you connect a custom domain through Cloudflare: choose “Uses Cloudflare or similar proxy” when you add the domain. Cloudflare can then add the header for you, as in the Cloudflare tab. If that changes, the header and value above are the same wherever you set them.

A CSP can also go in index.html as a meta tag, which works on any host. Meta-tag policies can’t use frame-ancestors or report-only mode, so test the policy in a local build first.

index.html
<!-- index.html, inside <head>, before any script -->
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://YOUR-PROJECT.supabase.co; font-src 'self' data:; connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co; base-uri 'self'; form-action 'self'; object-src 'none'">

Live posture findings come with the header or setting to change. PatchLabs never changes your hosting or your repo.

How PatchLabs finds it

live posture · report excerpt
[LOAD]    https://app.example.com/ → 200
          content-security-policy
          not sent                       FLAG
          report-only seen: no
synthetic exampleLive posture · read from the home page’s response

Live posture reads the final response of its one page load and looks for a Content-Security-Policy header, or a <meta http-equiv="Content-Security-Policy"> tag in the HTML. A report-only header on its own doesn’t pass, because it blocks nothing. It doesn’t grade how strict the policy is: a policy with 'unsafe-inline' scripts passes this check, so review yours against the starter above.

When the pattern is intended

A static page with no sign-in, no user content and no third-party scripts gains little from a CSP. Any app with accounts gains a lot.

Why it’s rated medium

Medium: nothing leaks from a missing CSP alone, but it is the difference between a cross-site scripting bug that reads every session and one that can’t send anything anywhere. 15 points in the Live posture grade.

Guides

Free check

Does your site have PL-030?

Type your site’s address. The free headers check loads your public home page once, like any visitor, and grades HTTPS, the certificate, security headers, cookie flags and mixed content. No sign-up, and the result isn’t stored.

Free, no sign-upLoads your public home page onlyHow Live posture works

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.