As a developer focused on building secure and efficient web applications, I often find myself deep in the weeds of browser security. One of the most common—and initially confusing—hurdles developers face when implementing a Content Security Policy (CSP) is the sudden flood of console errors about inline scripts and styles being blocked, even when 'unsafe-inline' is present.
If you've ever seen this error, you're in the right place:
Refused to execute inline script because it violates the following Content Security Policy directive... Note that
'unsafe-inline'is ignored if either a hash or nonce value is present in the source list.
Let's demystify this. It's not a bug; it's a critical security feature working exactly as designed.
What is Content Security Policy (CSP)?
Think of CSP as a security guard for your website. You provide it with a strict list of rules (a policy) that tells the browser which sources are allowed to load resources like JavaScript, CSS, fonts, and images. Its primary job is to mitigate and report certain types of attacks, most notably Cross-Site Scripting (XSS).
A simple script-src policy might look like this:
script-src 'self' https://trusted-cdn.com;
This tells the browser: "Only execute scripts that come from my own domain ('self') or https://trusted-cdn.com."
The Heart of the Conflict: nonce vs. 'unsafe-inline'
The problem arises when we mix two specific directives:
-
'unsafe-inline': This is the lenient directive. It tells the browser, "It's okay to execute any script found inside a<script>...</script>tag or any style inside a<style>...</style>tag directly in the HTML." As the name implies, it's considered unsafe because if an attacker can inject HTML into your page, they can easily inject a malicious script. -
'nonce-rAnd0mStr1ng': This is a much stricter and more secure directive. A "nonce" (number used once) is a unique, randomly generated string that your server produces for every single HTTP response. You then add this same nonce as an attribute to your legitimate inline script and style tags.Your CSP header would look like this:
script-src 'self' 'nonce-rAnd0mStr1ng';And your HTML would contain:
<script nonce="rAnd0mStr1ng"> // This is a trusted, server-generated inline script console.log("This will run!"); </script> <script> // This script was NOT generated by the server and will be blocked alert("This will NOT run!"); </script>
Here is the golden rule: When the browser sees a nonce (or a hash) in a policy, it completely ignores the 'unsafe-inline' directive for that policy.
Why? Because the two directives are fundamentally opposed. One says, "Trust anything inline," while the other says, "Trust only the inline code that has this secret one-time password." The browser prioritizes the stricter, more secure rule.
Common Violations You're Probably Seeing
When you enable a nonce-based CSP, you’ll suddenly discover just how much inline code is in your application. The violations usually fall into these categories:
- Inline
<script>Tags: Often from debug tools, analytics snippets, or dynamically injected JavaScript. - Inline Event Handlers: Attributes like
onclick,onsubmit, andonmouseoverare a form of inline script and will be blocked. - Inline
<style>Tags: CSS defined directly within<style>...</style>blocks. - Inline
styleAttributes: CSS applied directly to an element, like<div style="color: red;">.
The Fix: Choosing Your Strategy
You have two main paths forward.
Strategy 1: The Secure & Recommended Path - Embrace the Nonce
This is the best practice. It involves making your application fully compliant with your strict policy.
1. Add the Nonce to All Legitimate Inline Tags:
Your server-side code (e.g., in your Laravel master layout) must add the generated nonce to every <script> and <style> tag it renders. In Laravel, you can use the @csp_nonce Blade directive if you're using a package that provides it, or simply inject the variable yourself.
// In a middleware
$nonce = base64_encode(random_bytes(16));
View::share('csp_nonce', $nonce);
// Then set the header with $nonce<style nonce="{{ $csp_nonce }}">
.important-notice {
color: #b62f24;
}
</style>
<script nonce="{{ $csp_nonce }}">
// Your trusted JS code
</script>2. Refactor Inline Event Handlers:
This is non-negotiable. You must move away from onclick.
Before:
<button id="refreshBtn" onclick="refreshCaptcha()">Refresh</button>After:
<button id="refreshBtn">Refresh</button>
<script nonce="{{ $csp_nonce }}">
document.addEventListener("DOMContentLoaded", function () {
const refreshButton = document.getElementById("refreshBtn");
if (refreshButton) {
refreshButton.addEventListener("click", function refreshCaptcha() {
// Your logic to refresh the captcha
});
}
});
</script>3. Eliminate Inline style Attributes:
Move all styling into CSS classes defined in a file or a nonced <style> tag.
Strategy 2: The Pragmatic (But Less Secure) Fallback - Remove the Nonce
If you're dealing with legacy code or third-party libraries that you can't easily change, making everything nonce-compliant can be a massive undertaking. In this case, you can opt for a less secure policy.
Simply remove the 'nonce-...' from your CSP header.
Your policy would change from this:
script-src 'self' 'nonce-rAnd0mStr1ng' 'unsafe-inline' https:;
To this:
script-src 'self' 'unsafe-inline' https:;
⚠️ Security Warning: This makes your application vulnerable to inline script injection if an XSS flaw exists anywhere. By doing this, you're accepting the risk that
'unsafe-inline'brings. This should be a temporary measure or a last resort.
Conclusion
Implementing a strong Content Security Policy is a huge step toward securing your web applications. The conflict between nonce and 'unsafe-inline' is a fundamental concept designed to prevent you from accidentally weakening your own rules.
While it can be challenging, the recommended path is always to embrace the nonce and refactor your code to be compliant. It forces cleaner separation of concerns and results in a far more robust and secure application.
Stay secure and happy coding!