Content Security Policy (CSP) is a powerful browser mechanism designed to prevent and mitigate common web vulnerabilities such as Cross-Site Scripting (XSS) and Clickjacking. CSP allows developers to specify which sources of content are trusted by the application. This guide will walk you through how CSP works, and how to use it to protect against Clickjacking with frame-ancestors
. Real-world examples and practical tips are included to help you implement CSP securely.
Mitigating XSS with Content Security Policy
CSP headers control which scripts can be executed in the browser. By restricting script execution, you can block malicious scripts injected via XSS or SSTI.
A basic CSP looks like following:
Content-Security-Policy: default-src 'self'; script-src 'self';
What it means:
default-src 'self'
This sets the default policy for all content types (like images, fonts, frames, etc.) to only load from the same origin as the site itself.'self'
refers to the same domain that served the page.script-src 'self'
This specifically restricts where JavaScript can be loaded from. In this case, scripts are only allowed from the same origin (no external JS libraries like CDN-hosted jQuery, for example).
This CSP policy blocks any attempt to load or execute scripts from untrusted sources, making it highly effective against cross-site scripting (XSS) attacks.
But this CSP is very strict and will not allow developers to execute JS that are needed to run the application.
However, you may also come across poorly implemented CSPs
One of the core objectives of CSP is to prevent the execution of untrusted scripts, especially those injected by attackers via XSS. However, using the 'unsafe-inline'
directive in your CSP header essentially undoes this protection.
What 'unsafe-inline'
Does
When you include 'unsafe-inline'
in your script-src
directive:
httpCopyEditContent-Security-Policy: script-src 'self' 'unsafe-inline';
You are allowing:
- Inline
<script>
tags to execute freely. - JavaScript inside HTML event handlers (
onclick
,onmouseover
, etc.). - Attacker-injected scripts (in many XSS cases) to run without restriction.
Allowing inline scripts means that:
- Any script injected into the page through an XSS vulnerability can still run.
- Your CSP no longer offers protection against script injection.
- The browser treats inline scripts as “trusted” regardless of where they come from.
In almost all real-world applications, there will be a need to execute certain scripts to allow for its smooth functioning. To permit the execution of specific inline JavaScript while maintaining a strong Content Security Policy (CSP), it’s advisable to avoid the use of ‘unsafe-inline’. Instead, consider the following more secure methods:
- Nonce
- Hashes
In Part 2, we’ll deep-dive on how to configure CSP for XSS mitigation using nonces and hashes
CSP to Prevent Clickjacking
Clickjacking is an attack where users are tricked into clicking on something different from what they perceive, typically by overlaying a hidden iframe.
You can prevent clickjacking using Content Security Policy (CSP) by setting the frame-ancestors
directive. This directive specifies which origins are allowed to embed your site using <iframe>
, <frame>
, or <object>
tags.
Use Cases of CSP for Clickjacking
- Prevent clickjacking
- Allow trusted partners to embed your site
1. Completely Block Framing
To prevent your site from being embedded in any iframe, set the frame-ancestors
directive to 'none'
:
Content-Security-Policy: frame-ancestors 'none';
Effect:
- Prevents any website from embedding your site in an iframe.
- If an attacker tries to embed your site for clickjacking purposes, the browser will block the iframe.
2. Allow Framing from Trusted Sources Only
If you need to allow your site to be embedded only by specific trusted domains (e.g., your own domain), you can specify them like this:
Content-Security-Policy: frame-ancestors 'self' https://trusted-domain.com;
Effect:
'self'
allows embedding only from the same origin.https://trusted-domain.com
allows embedding from the specified trusted site only.- Any other origin trying to embed the site will be blocked by the browser.
Example Scenario
- Clickjacking Attack Attempt:
- Attacker creates a hidden iframe of your site and overlays invisible buttons.
- Victim unknowingly clicks the hidden button while interacting with what looks like a legitimate interface.
With CSP frame-ancestors 'none'
:
- Browser detects that the frame source is not allowed.
- The iframe is blocked, and the attack fails.
Why CSP frame-ancestors
is Better Than X-Frame-Options
CSP frame-ancestors | X-Frame-Options |
---|---|
More flexible (can specify multiple trusted origins) | Only allows DENY , SAMEORIGIN , or one trusted URL |
Part of modern CSP standard | Legacy header, less flexible |
Allows defining different framing policies per page | Same policy for the whole site |
To illustrate the difference between how Content Security Policy (CSP) and X-Frame-Options handle frame embedding, consider the following scenarios:
1. X-Frame-Options Validation:
X-Frame-Options primarily validates the top-level frame, determining whether the content can be embedded based on the immediate parent frame’s origin.
+-------------------+
| Top-Level Frame | <-- Checked by X-Frame-Options
| (example.com) |
| |
| +---------------+ |
| | Subframe | |
| | (attacker.com)| <-- Not checked by X-Frame-Options
| | | |
| | +-----------+ | |
| | | Subframe | | |
| | | (victim.com)| |
| | +-----------+ | |
| +---------------+ |
+-------------------+
In this structure, if victim.com
sets X-Frame-Options: SAMEORIGIN
, it prevents its content from being embedded directly by attacker.com
at the top level. However, attacker.com
can embed victim.com
within a subframe, as X-Frame-Options doesn’t validate beyond the top-level frame.
2. CSP’s frame-ancestors
Validation:
CSP’s frame-ancestors
directive validates the entire frame hierarchy, ensuring that all ancestor frames meet the specified policy.
+-------------------+
| Top-Level Frame | <-- Checked by CSP frame-ancestors
| (example.com) |
| |
| +---------------+ |
| | Subframe | <-- Checked by CSP frame-ancestors
| | (attacker.com)| |
| | | |
| | +-----------+ | |
| | | Subframe | <-- Checked by CSP frame-ancestors
| | | (victim.com)| |
| | +-----------+ | |
| +---------------+ |
+-------------------+
In this scenario, if victim.com
sets Content-Security-Policy: frame-ancestors 'self'
, it ensures that every ancestor frame in the hierarchy originates from victim.com
. Consequently, attacker.com
cannot embed victim.com
at any level within its frame structure, as it doesn’t satisfy the frame-ancestors
policy.
This distinction highlights that while X-Frame-Options offers basic protection against framing by unauthorized top-level domains, CSP’s frame-ancestors
provides a more robust security measure by validating the entire chain of embedding, thereby offering enhanced protection against clickjacking and other framing attacks.
Best Practice
- Use
frame-ancestors 'none'
unless you specifically need to allow trusted embedding. - Remove
X-Frame-Options
if you’re already using CSPframe-ancestors
(since CSP overrides it).
That’s all for this blog post. See you in part 2 where I will cover a detailed explanation of how nonce and hashes in CSP works and implemented.
Until then, happy defending!!