Any site can frame the page
Neither frame-ancestors nor X-Frame-Options is set, so another site can load your app invisibly inside its own.
- Severity
- Low
- Who can exploit it
- Any website that frames 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
By default, any website can show your page inside an <iframe>. Clickjacking uses that: the attacker’s page loads yours, makes it transparent, and lines up a button the visitor thinks is theirs (“Claim your prize”) over a button in your app (“Delete account”, “Make public”, “Approve”). The visitor is signed in to your app, so the click counts.
PL-031 means your page sends neither Content-Security-Policy: frame-ancestors … nor X-Frame-Options, the two headers that tell browsers who may frame it.
Why AI builders produce it
Framing protection is a header, so it sits outside the code a generator writes. It also gets left out on purpose sometimes, because the builder’s own preview shows the app in a frame, and a header that blocks framing would break the preview.
What we see
A synthetic example for app.example.com, shortened. Not taken from any real site.
$ curl -sI https://app.example.com
HTTP/2 200
content-type: text/html; charset=utf-8
# no x-frame-options, and no content-security-policy with frame-ancestors
Check it yourself
curl -sI https://your-app.com | grep -iE 'x-frame-options|content-security-policy'
# passes with either:
# x-frame-options: DENY (or SAMEORIGIN)
# content-security-policy: ... frame-ancestors 'none' ...
<!-- try it: save as frame-test.html, open it in your browser -->
<iframe src="https://your-app.com" width="800" height="600"></iframe>
<!-- if your app appears, any site can do this -->
The fix
Send both: frame-ancestors is the modern control and wins where it is supported, and X-Frame-Options covers older browsers. Use 'none' / DENY unless your own site frames the app, then 'self' / SAMEORIGIN. If you already have a CSP (PL-030), add the directive to it rather than sending a second policy.
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Vercel
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
Then add frame-ancestors 'none' to your Content-Security-Policy value in the same file. Put vercel.json at the project root and redeploy.
Netlify
# public/_headers (Vite copies public/ into the publish folder)
/*
X-Frame-Options: DENY
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 X-Frame-Options to:
DENY
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.
Neither header works as a <meta> tag: browsers ignore X-Frame-Options in HTML, and frame-ancestors is not allowed in a meta-tag CSP. This one needs a real header, so Cloudflare in front is the route.
Live posture findings come with the header or setting to change. PatchLabs never changes your hosting or your repo.
How PatchLabs finds it
[LOAD] https://app.example.com/ → 200 x-frame-options not sent frame-ancestors not set FLAG
Live posture reads both headers from the final response of its one page load. Either one passes: X-Frame-Options of DENY or SAMEORIGIN, or a frame-ancestors directive in an enforced Content-Security-Policy. It never frames your page itself.
When the pattern is intended
Embeddable widgets, such as a booking form other sites put in an iframe, need framing. Allow the sites that embed you by name (frame-ancestors https://partner.example) rather than everyone, and keep account and admin pages unframeable.
Why it’s rated low
Low: it needs a signed-in visitor to be lured to the attacker’s page and to click where expected. For apps with one-click destructive actions it deserves more attention than the grade gives it (5 points).
Related findings
No Content-Security-Policy
Your page sends no Content-Security-Policy, so any script that gets onto it can run and send data anywhere.
PL-032No X-Content-Type-Options: nosniff
Browsers may guess a file’s type from its contents, so an uploaded “image” can be run as a script or page.
PL-036No Referrer-Policy
Your page sends no Referrer-Policy, so the browser’s default decides how much of your URLs other sites see.
Guides
Free check
Does your site have PL-031?
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.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.