Written by the HackerMe team (Agura); we build HackerMe, an automated website security scanner. Each check is labeled: safe on any public site (it reads headers every visitor already gets), your own sites only, or maintainer check (needs server or code access).
Clickjacking (UI redress) hides your real page under something that looks harmless and tricks a visitor into clicking a control they did not mean to hit. The classic setup is an invisible iframe. Framing defenses do not replace authentication or CSRF controls; they limit who may embed the page. Background: the OWASP Clickjacking Defense Cheat Sheet, MDN: X-Frame-Options, MDN: CSP frame-ancestors and OWASP WSTG: Testing for Clickjacking.
1. Read the framing headers (safe on any public site)
curl -s -D headers.txt -o body.html https://example.com/account head -1 headers.txt grep -i -E '^(location|x-frame-options|content-security-policy):' headers.txt This is a normal GET that keeps the headers and the body. curl -I sends a HEAD request instead, and some servers and proxies answer HEAD with different headers than the GET a browser makes (MDN: HEAD). Before reading the result, confirm what you got: the real page, a redirect to login (a 3xx with Location), or an error page. An unauthenticated request to /account often returns the login redirect, and its headers tell you about the redirect, not the account page.
- Expected: either
X-Frame-OptionswithDENYorSAMEORIGIN, or aContent-Security-Policythat includesframe-ancestors(for example'none'or'self'), or both. - Finding: neither header restricts framing. If both are absent, browsers allow other sites to embed the document (MDN: X-Frame-Options). A missing header is a gap to fix, not proof that someone can make a user perform an action: that also depends on the page having a state-changing control, the user being signed in inside a frame (cookies, SameSite), and any confirmation step.
2. Confirm the policy is an HTTP header, not a meta tag (safe on any public site, then maintainer check)
X-Frame-Options and CSP frame-ancestors are enforced only as HTTP response headers. A <meta http-equiv="X-Frame-Options" ...> tag has no effect (MDN). The same rule applies to frame-ancestors: it is not supported in a <meta> CSP (MDN: frame-ancestors).
grep -i -E 'http-equiv=["'\'']?x-frame-options|frame-ancestors' body.html Using the files from check 1: if the restriction shows up only in body.html and not in headers.txt, it does nothing. Also keep in mind that a CDN or reverse proxy may not keep the header. Proxies sometimes strip or rewrite response headers (OWASP Clickjacking Defense Cheat Sheet).
3. Read the value carefully (safe on any public site)
| What you see | What it usually means |
|---|---|
X-Frame-Options: DENY | No framing, including same-origin. |
X-Frame-Options: SAMEORIGIN | Framing only when every ancestor shares the page's origin (MDN). |
X-Frame-Options: ALLOW-FROM … | Obsolete. Modern browsers ignore the header entirely when they see this directive (MDN). Prefer CSP frame-ancestors. |
Content-Security-Policy: … frame-ancestors 'none' | Similar intent to DENY. |
frame-ancestors 'self' | Similar intent to same-origin framing for this directive. |
frame-ancestors 'self' https://partner.example | Explicit allow list. Review every entry. |
CSP frame-ancestors does not fall back to default-src. A policy that sets default-src 'none' still allows embedding by anyone unless frame-ancestors is set (MDN: frame-ancestors).
Flag ALLOW-FROM, an empty restriction, or a frame-ancestors list with origins you do not recognize.
4. Check the pages that matter, not only the homepage (safe on any public site)
Framing risk is highest on pages where a single click changes something: approve a transfer, change an email address, disable MFA, confirm a delete. A protected marketing homepage with an unprotected /settings page is a common split.
for p in / /login /account /settings /checkout /admin; do printf "%s " "$p" curl -s -o /dev/null -D - "https://example.com$p" | grep -i -E '^(HTTP/|location|x-frame-options|content-security-policy):' echo done What you are looking for is a homepage that is locked down next to a state-changing page that is not. If a sensitive path answers with a redirect to login, you are reading the redirect's headers; check the page itself with a signed-in test account. API-only endpoints matter less here. Focus on documents a browser will render as a page. Still verify error pages and mobile routes; the OWASP testing guide notes that alternate or mobile variants are sometimes left without the same controls.
5. Load your own page in a local test frame (your own sites only)
Create a local HTML file and point the iframe at a page you own or are authorized to test:
<!DOCTYPE html> <html lang="en"> <head><title>Framing check (local)</title></head> <body> <p>Local framing check — use only on systems you own or may test.</p> <iframe src="https://example.com/account" width="800" height="600"></iframe> </body> </html> Open the file in your browser.
If your real page renders inside the frame, that is a finding. A blank frame is not proof of protection on its own: the URL may have failed to load, redirected, or errored. Open DevTools and confirm the browser refused the frame because of a framing policy; the Console names X-Frame-Options or frame-ancestors when that is the reason.
Even a confirmed block does not cover every UI-redress technique. Framing headers address frame-based clickjacking. They do not stop attacks that use a separate top-level window (sometimes discussed as DoubleClickjacking), which need action confirmation and server-side authorization instead (OWASP Clickjacking Defense Cheat Sheet).
6. Deployment and defense in depth (maintainer check)
Headers only help if they actually leave the server on the responses you care about.
nginx add_header inheritance. With the default inheritance model, add_header directives from a parent block are inherited by a location only when that location defines no add_header of its own. One extra add_header inside a location replaces the inherited set for that location; nginx does not merge them unless you opt into newer behaviour. From nginx's headers module docs: inheritance can be changed with add_header_inherit starting in nginx 1.29.3 (merge appends parent values). On older builds, put shared security headers in an included snippet and include that snippet in every block that sets any add_header, or avoid adding headers only in nested locations. Also prefer always when you need the header on non-success status codes; without it, add_header applies only to a fixed list of codes listed in the same docs.
SameSite cookies. Cookies marked SameSite=Lax or Strict are withheld from many cross-site iframe requests, which can reduce authenticated clickjacking. SameSite alone does not block framing, and it does not help when the attack does not need a cookie (OWASP Clickjacking Defense Cheat Sheet).
Do not rely on weak frame-buster scripts alone. Simple if (top !== self) top.location = ... patterns have well-documented bypasses (nested frames, sandboxed iframes, and others) in the OWASP materials above. Prefer HTTP framing headers first.
Example: reading one result
$ curl -s -o /dev/null -D - https://example.com/ | grep -i -E '^(HTTP/|x-frame-options|content-security-policy)' HTTP/2 200 x-frame-options: SAMEORIGIN $ curl -s -o /dev/null -D - https://example.com/settings | grep -i -E '^(HTTP/|location|x-frame-options|content-security-policy)' HTTP/2 200 (Illustrative output.) Both responses are 200 pages, not redirects, so they are the pages themselves. The homepage restricts framing; /settings sends no framing policy. That is a finding to fix on the settings response (and any other sensitive HTML responses), then re-check. A clean homepage result alone would not have shown this gap.
Make it routine
Re-run these checks after reverse-proxy changes, CDN rule edits, new app routes and theme or CMS updates. Keep a short list of the paths you verify. A pass means those responses sent a framing policy when you looked. It is not a claim that the application is free of UI redress risk or that every client will behave the same way.
If you want an outside look at a domain you own or have explicit permission to test, HackerMe is here: https://brandingme.ai/app/r/z8jTS7DxaholZZWJLinBnA.
What framing edge case have you hit in production? We would like to hear it in the comments.
