csp

Content Security Policy (CSP): A Key Mitigation for XSS and Clickjacking

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

  1. 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-ancestorsX-Frame-Options
More flexible (can specify multiple trusted origins)Only allows DENY, SAMEORIGIN, or one trusted URL
Part of modern CSP standardLegacy header, less flexible
Allows defining different framing policies per pageSame 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 CSP frame-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!!

shreyapohekar

I’m Shreya Pohekar, a Senior Product Security Analyst at HackerOne. I enjoy sharing my thoughts and insights through blogging, turning complex security topics into engaging and accessible content for my readers.

Leave a Reply