SecHead
Scan a siteContact Us
How-To7 min read

How to Fix CSP Findings from a Penetration Test Report

Got a pentest report flagging your Content Security Policy as misconfigured? This guide walks through every common finding-unsafe-inline, unsafe-eval, missing nonces, and more-and shows you exactly how to fix them.

SL
Seven Labs · 1 July 2026
1,455 words

How to Fix CSP Findings from a Penetration Test Report

If you've just received a penetration test report and it flags your Content Security Policy (CSP) as misconfigured, you're not alone. CSP is the most commonly flagged HTTP security header in professional security assessments - not because developers are careless, but because CSP is genuinely difficult to get right. A policy that loads your site correctly in development can still be flagged as ineffective by a penetration tester, and they're usually correct.

This guide covers every common CSP finding that appears in pentest reports, explains why each finding is a real problem, and gives you the exact fix for each one. Scan your site with SecHead first to see your current CSP grade before diving in.


Why Pentest Reports Flag CSP

Penetration testers evaluate CSP against a clear standard: does this policy actually prevent an attacker from running arbitrary JavaScript in the browser, even if they find an injection point? A CSP that is present but doesn't meet that bar will be flagged as "misconfigured" or "ineffective" regardless of how long it took to write.

The most common finding reads something like:

"The application implements a Content Security Policy (CSP); however, the policy is misconfigured and does not provide effective protection against cross-site scripting (XSS) attacks. The script-src directive explicitly allows both 'unsafe-inline' and 'unsafe-eval', which significantly weakens the purpose of CSP."

That one paragraph covers the most common CSP failures. Let's fix each of them.


Finding 1: unsafe-inline in script-src

What the report says

"The script-src directive explicitly allows 'unsafe-inline', which permits the execution of inline JavaScript and inline event handlers. This allows an attacker who finds an injection point to execute arbitrary scripts without needing to load a file from an external source."

Why it matters

'unsafe-inline' tells the browser: "trust any <script> tag or onclick attribute on this page, regardless of where it came from." If an attacker can inject even one character of HTML (via a comment field, a URL parameter, a stored value in your database), they can run JavaScript.

This is the most critical CSP finding. A policy containing 'unsafe-inline' provides essentially zero XSS protection.

The fix: nonces

Replace 'unsafe-inline' with a nonce - a random, single-use token generated fresh on every page load and included in both the CSP header and each <script> tag the server renders.

CSP header:

Content-Security-Policy: script-src 'nonce-r4nd0mT0k3n' 'strict-dynamic';

HTML:

html
<script nonce="r4nd0mT0k3n">
  // your inline script here
</script>

The browser only executes scripts whose nonce matches the one in the header. An injected <script> tag won't have the nonce, so it won't run.

Generate a fresh nonce on every request - never reuse the same value across requests or the protection collapses.

The fix: hashes (alternative)

If you have fixed inline scripts that don't change, you can use their SHA-256 hash instead:

Content-Security-Policy: script-src 'sha256-abc123...';

Run echo -n 'your script content' | openssl dgst -sha256 -binary | base64 to generate the hash. This works well for small, static scripts but doesn't scale to dynamic content.


Finding 2: unsafe-eval in script-src

What the report says

"The script-src directive allows 'unsafe-eval', which permits the use of eval(), new Function(), setTimeout(string), and similar dynamic code evaluation. These constructs are common XSS exploitation techniques."

Why it matters

'unsafe-eval' permits JavaScript to evaluate strings as code at runtime. Even if you've blocked inline scripts with a nonce, an attacker who finds a DOM-based injection point can call eval('injected_string') and bypass your policy entirely.

The fix

Remove 'unsafe-eval' from your CSP. Modern JavaScript frameworks have moved away from requiring it:

  • React - doesn't need unsafe-eval in production builds
  • Vue 3 - the runtime compiler needs it, but if you're using single-file components pre-compiled via webpack/vite, you don't
  • Angular - uses the Ivy compiler which doesn't require unsafe-eval

If you're using a library that still requires unsafe-eval (some older template engines, certain charting libraries), contact the library maintainer or find an alternative. There's no way to safely allow eval() in a CSP - it's a binary choice.


Finding 3: Host-based allowlisting without strict-dynamic

What the report says

"The CSP relies primarily on host allowlisting without using modern hardening mechanisms such as nonces, hashes, or strict-dynamic. An attacker can abuse trusted hosts (CDNs, third-party script hosts) to bypass the policy."

Why it matters

A policy like:

Content-Security-Policy: script-src 'self' https://cdn.example.com https://ajax.googleapis.com;

...looks reasonable but is bypassable. If any file on cdn.example.com or ajax.googleapis.com can be loaded as a script, an attacker who can inject a <script src="https://cdn.example.com/angular/angular.min.js"> tag can use Angular's template injection to run arbitrary code. This is a documented technique called a CSP bypass via trusted CDN endpoints.

The fix

Combine nonces with 'strict-dynamic':

Content-Security-Policy: script-src 'nonce-r4nd0mT0k3n' 'strict-dynamic';

'strict-dynamic' tells the browser: "trust scripts loaded by a nonce-bearing script, and nothing else." This eliminates the CDN allowlist bypass entirely - your server controls exactly which scripts execute via the nonce, and only those scripts can load further scripts.


Finding 4: Missing require-trusted-types-for 'script'

What the report says

"The CSP lacks the require-trusted-types-for 'script' directive, which would otherwise help prevent DOM-based XSS sinks from being abused."

Why it matters

DOM-based XSS occurs when untrusted data is passed into dangerous DOM sinks - innerHTML, document.write(), eval() - without sanitization. Trusted Types is a browser-level control that prevents this class of attack by requiring all data passed to DOM sinks to go through a registered, audited "policy" function.

The fix

Add to your CSP:

Content-Security-Policy: ...; require-trusted-types-for 'script'; trusted-types default;

Then, in your JavaScript, replace raw DOM writes with Trusted Types-compatible code:

Before:

js
element.innerHTML = userInput; // dangerous

After:

js
const policy = trustedTypes.createPolicy('default', {
  createHTML: (input) => DOMPurify.sanitize(input)
});
element.innerHTML = policy.createHTML(userInput);

Browser support is good in Chromium-based browsers (Chrome, Edge). Firefox support is in progress. It's safe to deploy today because unsupported browsers simply ignore the directive.


Finding 5: No default-src fallback

What the report says

"The CSP does not define a default-src directive, which means fetch directives that are not explicitly set inherit no fallback restriction."

The fix

Always start your CSP with a restrictive default-src and then loosen specific directives as needed:

Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-r4nd0mT0k3n' 'strict-dynamic';
  style-src 'self' 'nonce-r4nd0mT0k3n';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';

default-src 'none' blocks everything not explicitly permitted. Build up from there.


The Remediated Policy

Here's a complete, pentest-passing CSP for a typical web application (server-rendered with a nonce):

Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self' 'nonce-{RANDOM}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  media-src 'none';
  object-src 'none';
  frame-src 'none';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  require-trusted-types-for 'script';
  trusted-types default;

Replace {RANDOM} with a fresh cryptographically random value on every request (at least 128 bits of entropy).


Deploying in Report-Only Mode First

Before switching your live CSP, use Content-Security-Policy-Report-Only to catch violations without blocking anything:

Content-Security-Policy-Report-Only: default-src 'none'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; report-uri /csp-violation-report;

Check your violation endpoint for a week. Fix any legitimate sources that show up in the reports. Then switch to Content-Security-Policy once violations drop to zero.


Verifying the Fix

After implementing your changes:

  1. Run a free scan on SecHead - a well-configured CSP will push your grade to A or A+
  2. Open your browser's DevTools console - a correctly deployed nonce-based policy will show no CSP violation errors
  3. Ask your pentest team to re-test, or use your browser's CSP evaluator to check the final policy string

For a deeper understanding of why each directive exists, see our full Content Security Policy explainer. To see what other headers are commonly missed alongside CSP, the complete security headers checklist covers the full picture.


Summary

Pentest findingRoot causeFix
unsafe-inline presentInline scripts not using noncesMigrate to nonce-based CSP
unsafe-eval presentDynamic code evaluation allowedRemove unsafe-eval, audit libraries
Host allowlist onlyCDN bypass attacks possibleAdd strict-dynamic + nonces
Missing Trusted TypesDOM-based XSS sinks unprotectedAdd require-trusted-types-for 'script'
No default-srcUncovered directives fall back to permissiveAdd default-src 'none' as foundation

A properly implemented nonce-based CSP eliminates the most critical class of client-side injection attacks. It takes work to migrate to - but it's the only CSP configuration that a competent penetration tester won't flag.

Related articles

Free tool

Check your own security headers

Instant grade, plain-language explanations, and a full remediation plan - no signup needed.

Scan your site now →