A sensitive web page can have sound authentication and authorization yet still be exposed through another site’s interface. If an attacker can place that page inside a transparent or disguised frame, a signed-in user may believe they are clicking one control while their click reaches a different control in the framed application.

That attack class is clickjacking, also called UI redressing. The browser is doing what both pages request; the security failure is that the sensitive application allowed an untrusted page to become part of its user interface.

A framing policy changes that trust boundary. For pages that don’t need embedding, deny framing. For pages that do, name the origins that are allowed to embed them. This article develops that model, shows the relevant response headers, and covers the limits that still require application-level controls.

Clickjacking abuses presentation, not authentication

Consider an account page with a button that enables a sensitive feature. The user has already signed in, so requests from that page carry the user’s normal authenticated session.

Now suppose an unrelated site can embed the account page in a frame. The unrelated site can arrange its own visible content around or over the framed page so the user has a misleading picture of the action under the pointer. A click intended for the visible decoy can land on the framed application’s control.

The key point is that the attacker does not need the user’s password or session cookie. The browser sends the authenticated request because the user is interacting with the genuine application. The attacker is manipulating the presentation layer around it.

The defensive goal is therefore specific: stop untrusted ancestors from embedding pages whose user interface can trigger meaningful actions.

This control does not replace authorization. The server must still check that the signed-in principal is permitted to perform each action. It also does not replace cross-site request protections, output encoding, or confirmation for unusually sensitive operations. Those controls address different paths to unwanted actions.

Treat framing as an explicit trust decision

A useful default is simple:

Content-Security-Policy: frame-ancestors 'none'

The frame-ancestors directive in Content Security Policy (CSP) restricts which ancestors may embed the protected resource in a frame, iframe, object, or embed. The value 'none' means that no ancestor is accepted.

If the application intentionally embeds its own pages, a same-origin policy may fit:

Content-Security-Policy: frame-ancestors 'self'

If a separate trusted site must embed the page, list that origin according to CSP source-expression syntax:

Content-Security-Policy: frame-ancestors 'self' https://portal.example

This is not a statement that portal.example is generally trustworthy. It grants one specific capability: pages matching that source may participate in the ancestor chain for this protected resource. Compromise of an allowed framing origin can therefore weaken the protection.

Keep the allowlist as narrow as the product permits. A broad framing policy increases the number of sites whose interface integrity becomes part of the protected page’s security assumptions.

The browser checks the ancestor chain

Framing can be nested. A page from one origin can contain a frame from a second origin, which can in turn attempt to embed the sensitive application.

CSP frame-ancestors is designed around that reality. The browser checks the origins in the ancestor chain against the protected page’s policy. An unapproved ancestor causes the embedding to be blocked.

That distinction matters when designing integrations. It is not enough to approve only the page immediately surrounding the application if an outer page can come from an untrusted origin.

For example, suppose a support console at https://support.example is permitted to embed a billing page. If the support console itself can be embedded by arbitrary sites, the complete nesting arrangement needs scrutiny. A narrow frame-ancestors policy on the billing page can reject an ancestor that is outside its allowed set.

The protected application should send the policy in its HTTP response. CSP Level 3 specifies that frame-ancestors is ignored when delivered through a meta element, so placing this directive only in HTML metadata does not provide the intended framing restriction.

Use X-Frame-Options as a compatibility layer when needed

X-Frame-Options is an older HTTP response header for controlling framing. Two values remain useful for simple policies:

X-Frame-Options: DENY

and:

X-Frame-Options: SAMEORIGIN

DENY rejects framing. SAMEORIGIN permits framing under the same-origin rule implemented for that header.

For applications that support browsers where CSP frame-ancestors coverage cannot be assumed, sending a compatible X-Frame-Options header can provide defense in depth. Keep the two policies aligned. A page intended to reject all framing can send both:

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

A page intended for same-origin framing can use:

Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN

Do not use the historical ALLOW-FROM form as a substitute for a CSP allowlist in modern application design. RFC 7034 documented inconsistent implementation around X-Frame-Options, and CSP provides the more expressive ancestor policy.

Also avoid JavaScript “frame busting” as the primary control. Script-based logic runs inside the document and has historically depended on browser and embedding behavior. A browser-enforced response policy places the decision at the framing boundary instead.

Apply the policy to every sensitive response

A common implementation mistake is protecting only a landing page. Clickjacking protection matters on the response that contains the actionable interface.

Suppose /account sends a restrictive policy but /account/email/change renders a separate confirmation screen without it. An attacker who can frame the second route still has an interface to target.

Centralized response-header configuration reduces this kind of gap. If most application pages should never be framed, set the restrictive policy at a shared middleware, reverse proxy, or equivalent response layer. Add narrowly reviewed exceptions only for routes that have a genuine embedding requirement.

Be careful with errors and alternate render paths as well. Authentication prompts, consent screens, administrative actions, payment confirmations, and account-recovery interfaces can be especially consequential when they are interactive. A policy that appears only on the normal success response can leave other representations outside the intended boundary.

Static assets do not generally need the same treatment because they do not present an interactive document in the same sense. Focus verification on HTML and other embeddable document responses that form application interfaces.

Test both rejection and intended embedding

Configuration review alone can miss headers added on one route but omitted on another. Verification should exercise the deployed response path, including proxies and content delivery layers that can alter headers.

For a page that must reject all framing, check that its final HTTP response contains the intended policy:

Content-Security-Policy: frame-ancestors 'none'

Then test from a separate origin that attempts ordinary iframe embedding. The protected document should not become an interactive framed page.

If embedding is intentionally supported, test both sides of the boundary: an approved origin should work, and an unapproved origin should be rejected. Include nested framing in the test plan when the integration architecture permits nested frames.

Browser developer tools can help confirm that a framing failure comes from the policy rather than from an unrelated network or application error. Automated browser tests are useful for high-value routes because they can catch accidental header removal during deployment changes.

Also inspect the final response rather than only application source code. A middleware rule that looks correct has little value if a proxy overwrites it or a separate service serves the sensitive route without it.

Framing permission creates a security dependency

Some products legitimately need cross-origin embedding: hosted dashboards, support tools, administrative portals, and partner integrations are common examples. In those cases, 'none' is not compatible with the product requirement.

The decision becomes a trust-boundary exercise. If https://partner.example may frame a sensitive page, that partner origin can control visual content around the frame. Its compromise, unsafe delegated content, or overly broad script execution can affect the integrity of the combined interface.

Reduce that dependency where practical. Expose only the pages needed for the integration rather than making the entire account application embeddable. Consider a separate embedded surface with fewer privileges and fewer sensitive actions. Require fresh confirmation outside the frame for operations whose impact justifies the extra friction.

A simpler same-origin or no-framing policy is preferable when cross-origin embedding has no concrete product need. An allowlist is justified when embedding is a deliberate feature and the allowed origins can be managed as security-sensitive dependencies.

Framing controls have clear limits

A framing policy reduces clickjacking risk by controlling embedding. It does not prove that an allowed framing site presents the interface honestly. If an approved origin is compromised, the policy can still permit that origin to surround the protected page.

It also does not stop deceptive links that open the application as a top-level page, phishing pages that imitate the application, or malicious browser extensions with powerful local privileges. Those threats require different controls.

Server-side authorization remains mandatory. For actions with high impact, reauthentication, explicit confirmation, transaction details, or other context-appropriate safeguards can reduce damage from a broader set of interface and session threats.

CSP itself contains many directives, but frame-ancestors has a narrow job. Do not assume that default-src 'none' implicitly supplies the same restriction. CSP Level 3 states that frame-ancestors does not fall back to default-src; configure it explicitly.

Make the framing boundary visible in design reviews

Treat “which origins may display this page inside their interface?” as a property of each sensitive web surface.

For applications with no embedding requirement, a deny policy is easy to reason about and easy to test. For intentional embedding, document the approved origins, keep the list narrow, test the full ancestor arrangement, and review each exception as a security dependency.

The practical next step is to inventory interactive routes that perform sensitive actions and inspect their deployed response headers. A framing policy is small configuration, but it closes a distinct trust boundary that authentication and authorization do not cover on their own.