PL-031lowLive posture

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 (response, shortened)
$ 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

check-pl-031.sh
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' ...
frame-test.html
<!-- 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.

the headers
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

Vercel

vercel.json
{
  "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
# 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:

X-Frame-Options · value
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

live posture · report excerpt
[LOAD]    https://app.example.com/ → 200
          x-frame-options    not sent
          frame-ancestors    not set     FLAG
synthetic exampleLive posture · read from the home page’s response

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).

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.

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.