Imagine a button on your site that approves a payment, changes a setting or grants a permission. Now imagine an attacker's page that loads your site in a transparent frame, lines that button up under a harmless looking "Play video" link, and waits for a click.
That is clickjacking, and CWE-1021 is the weakness behind it. The web application does not restrict, or incorrectly restricts, frame objects or UI layers that belong to another application or domain. MITRE lists it as a Base weakness that is not tied to a language or technology, with web based occurrences.
Every web application with a state changing page is a candidate. So are device admin interfaces, desktop apps with web content, and mobile screens that can be overlaid.
How it happens
Browsers will, by default, let one page embed another. That is useful for legitimate widgets and dangerous for your sensitive pages. If you send no framing restrictions, any site can embed you.
The attack works because the user's clicks are real. They are signed in to your service, the frame loads with their session, and the click lands on a real control. The victim sees only the decoy. MITRE describes the impact as tricking a user into performing actions that are masked and hidden from their view.
The observed examples on the MITRE page show how widely the pattern appears:
- CVE-2017-7440: an email preview feature in a desktop application allowed clickjacking through a crafted email message.
- CVE-2017-5697: a hardware and firmware product with insufficient clickjacking protection in its web user interface.
- CVE-2017-4015: clickjacking in a data loss prevention product, addressed via an HTTP response header.
- CVE-2016-2496: tapjacking in a mobile operating system permission dialog, which allowed access to private storage using a partially overlapping window.
The last one is worth noting. The same idea, a hidden or overlapping layer capturing taps, works outside the browser.
Here is the unprotected server, in Express:
// Vulnerable: no framing restrictions on any response
app.get("/account/delete", (req, res) => {
res.send(renderConfirmPage());
});
And a protected version:
// Fixed: forbid framing by other origins
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
res.setHeader("X-Frame-Options", "SAMEORIGIN");
next();
});
What an attacker gains
MITRE's consequences read like a menu of what a click can do, because the impact depends on the framed page:
- Gain privileges or assume identity. The click is made as the victim.
- Bypass protection mechanisms. Confirmation dialogs and permission prompts are clicked through without the user understanding.
- Read or modify application data. Whatever the hidden control does, it does with the victim's authority.
MITRE sums it up as impact that varies widely with the functionality of the underlying framed page. A framed "like" button is a nuisance. A framed "transfer funds" or "grant access" control is a serious loss.
How to find it
MITRE lists automated static analysis, SAST, as a way to find some instances. For this weakness, runtime checks are very quick and often more telling, because framing headers are frequently added or stripped by proxies and gateways.
- Request your sensitive pages with
curl -Iand look forX-Frame-Optionsor aContent-Security-Policycontainingframe-ancestors. If neither is present, the page can be framed. - Build a tiny local HTML file that embeds your page in an
iframeand open it in a browser while signed in. If your page renders inside the frame, so can an attacker's. - Check every page type, not only the login page. Settings, payment, permission and deletion pages matter most.
- Check pages inside objects, embeds and applets as well. MITRE notes the application is expected to restrict rendering there too.
- Test through your real entry point, including CDNs and gateways, not just the application server.
How to fix it
The MITRE page lists four implementation level defenses, which work well together.
- Use X-Frame-Options. It lets you restrict use of your application in overlays, frames and iframes, and indicate which domains may frame the content. MITRE notes there is a need to allow frames from multiple domains and that the mechanism is still evolving to cover that gap.
- Use a frame-breaker script as a legacy fallback. It helps browsers that do not support the header. MITRE also warns that this tactic has been circumvented, and that nested frames can defeat naive scripts. Treat it as a last resort, never as the main control.
- Use Content Security Policy. Depending on the implementation, use the
frame-ancestorsdirective or theframe-srcdirective to restrict where content may be embedded. It prioritizes valid sources through declarative policies, which suits a defense in depth approach. - Cover objects, embeds and applets. Do not stop at frames and iframes. Restrict whether the application may be rendered in those elements as well.
A few practical notes beyond the record:
- Pick the strictest value that works.
frame-ancestors 'none'for pages nobody should embed,'self'for same origin use, and an explicit allow list for real partners. - Set the header in one place, such as shared middleware or the edge, so new routes inherit it.
- For flows that must be embeddable, such as a payment widget, add friction to the sensitive action itself, such as a confirmation that cannot be triggered by a single click from a framed context.
- Add a test that fetches key pages and asserts the header is present.
Try it yourself
This demo shows a dynamic scan of a web application, including the response header checks that catch missing framing protection.
Click through it at your own pace, or open it full screen.
Web frameworks and admin products that you embed or run also have their own advisories. Run the free in-browser scan at gold.safeguard.sh/scan to check your dependencies. No login, the file stays on your machine, up to 100 dependencies.
Quick check
Why does clickjacking work even though the user is really clicking?
The click is real and lands on a real control in your page, loaded with the victim's session. The user just cannot see it, because it sits under a decoy.
Which two controls restrict who can embed a page?
The X-Frame-Options header and the frame-ancestors directive of a Content Security Policy. A frame-breaker script is only a legacy fallback and has been bypassed.
Is this only a browser problem?
No. MITRE's examples include a mobile permission dialog attacked through a partially overlapping window, and a desktop application's email preview.