Home › Security › CSP Blocked Inline Script — Nonce, Hash, and Fixes
Beginner 6 min · September 23, 2026
Content Security Policy Blocked Inline Script

CSP Blocked Inline Script — Nonce, Hash, and Fixes

CSP blocked your inline script and the page went quiet.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 16 min
  • ✓A site where you can set response headers
  • ✓Basic familiarity with HTML script tags and events
  • ✓Access to browser devtools console
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • A blocked inline script means your Content Security Policy has no script-src permission covering that block — the browser refused to run it
  • Don't fix it with unsafe-inline: bless specific scripts with a per-request nonce or a SHA-256 hash instead
  • Move behavior into external .js files served from your own origin; event handlers and javascript: URLs must go too
  • Roll out in report-only mode first with report-uri monitoring, and fix every violation report before enforcing
  • Keep object-src none, lock down base-uri, and review third-party scripts before adding them to your policy
✦ Definition~90s read
What is Content Security Policy Blocked Inline Script?

Content Security Policy is a W3C standard that lets your server instruct the browser what each page may load and execute. You deliver it in the Content-Security-Policy HTTP header (preferred) as directives: script-src for JavaScript, style-src for CSS, img-src, connect-src, object-src, base-uri, and more. Anything not permitted is blocked, with violations optionally reported back to you.

★
Picture a theater that only lets actors on stage if their names are on tonight's list.

The directive that bites first is script-src, because its default posture blocks two things most apps rely on: inline <script> blocks and inline event handlers (onclick, onload, javascript: URLs). Browsers can't distinguish them from injected attack code — which is exactly why blocking them defeats XSS.

Legitimate inline scripts must carry proof: a nonce (random per-request token in both header and tag) or a hash (SHA-256 of the exact contents, listed in the policy).

Nonces and hashes serve different shapes of code. A nonce blesses a script tag regardless of contents, so it suits dynamic server-rendered scripts — but it must be random per response and never reused, or an attacker who reads one response can reuse it.

A hash blesses exact contents, suiting stable snippets like analytics loaders — but any character change (even whitespace) breaks the match and re-blocks the script. External files from approved origins need neither — the cleanest long-term answer.

Rollout discipline decides whether CSP becomes protection or an outage. Content-Security-Policy-Report-Only logs violations without blocking — deploy it first with a report collector, fix every legitimate violation, then switch to enforcing. Teams that skip monitoring and enforce on day one rediscover every forgotten widget simultaneously, usually on a Friday.

Plain-English First

Picture a theater that only lets actors on stage if their names are on tonight's list. Your inline script is an actor who always walked on unannounced — and tonight the theater enforces the list, so the show goes quiet. Content Security Policy is that door list: the browser runs only scripts from approved sources or carrying an approved stamp. The fix isn't firing the doorman (which invites every impersonator) — it's getting your actors properly listed.

You deploy on Friday, open the site, and half the page is dead. Buttons don't respond, the checkout widget never loads, analytics flatlines. The console shows a wall of red: 'Refused to execute inline script because it violates the following Content Security Policy directive.' Nothing in your JavaScript changed. What changed is that the browser started enforcing a policy — yours, newly deployed or newly tightened — that your inline scripts don't satisfy.

Content Security Policy (CSP) is a browser-enforced allowlist delivered in an HTTP header (or meta tag). It tells the browser which sources may provide scripts, styles, images, and more — and by default, inline scripts and event-handler attributes are blocked even on your own pages. That default surprises teams whose apps grew up on onclick attributes and footer script blocks, but it's the control that neuters entire classes of cross-site scripting: injected markup can't execute when nothing unapproved runs.

The fix path is well-trodden: understand why the block fired, bless legitimate scripts with nonces or hashes (never unsafe-inline), refactor inline code into external files where it belongs, and roll the policy out through report-only monitoring before enforcing. This guide walks that path with production-tested steps — and shows how to tighten the policy into a real XSS defence instead of a checkbox header.

Why Your Inline Script Stopped Running

When a CSP script-src directive is present, the browser builds a guest list for JavaScript: files from listed origins may run, everything else is refused at parse time. Inline <script> blocks aren't on the list by default — not because the browser doubts your code, but because it can't distinguish your block from an attacker's injected one. Same for onclick attributes and javascript: URLs: they execute markup-embedded code with no origin to check, so the policy treats them as guilty until proven blessed.

The console message is your diagnostic: it names the violated directive and quotes the policy, telling you exactly which rule fired. 'Refused to execute inline script because it violates script-src' means the block lacked a matching nonce or hash — the fix is blessing the script, not loosening the directive. Resist the siren call of 'unsafe-inline': it re-admits every inline script including injected ones, deleting the XSS protection you deployed the policy for. It's the one token that turns the whole header decorative.

Start every CSP fix with an inventory, not an edit. Open each affected page, collect every console violation, and map each blocked script to its owner and purpose — you'll routinely find forgotten A/B test snippets, dead analytics loaders, and CMS-injected widgets nobody claims. Removing the dead code first shrinks the blessing work and the attack surface together. What remains gets a nonce, a hash, or a new home in an external file.

BASH
1
2
3
4
5
# Inventory every inline script and handler BEFORE touching the header
rg -l --glob '*.html' -e '<script[^>]*>' templates/ | tee /tmp/inline-pages.txt
rg -o --no-filename 'on(click|load|submit|error|mouseover)=' templates/ \
  | sort | uniq -c | sort -rn
rg -l 'javascript:' templates/ || echo 'OK: no javascript: URLs'
📊 Production Insight
A team 'fixed' violations with unsafe-inline in 10 minutes and closed the ticket. A stored-XSS flaw found months later executed freely — the policy that should have contained it was decorative. The quick fix cost them their safety net.
🎯 Key Takeaway
Inline code is blocked because it's indistinguishable from injected code. Inventory every blocked script, remove the dead ones, and bless the rest — never unsafe-inline.

Nonce vs Hash: Two Ways to Bless a Script

Nonces and hashes both prove a script is yours, but they fit opposite situations. A nonce blesses a tag regardless of contents, suiting dynamic server-rendered scripts — but it must be fresh per response, never reused. Generate at least 128 bits from a cryptographic source for every response, emit the identical value in the header and the tag, and mark nonced responses no-store so caches never replay a token. A static hardcoded nonce copied from a tutorial blesses attacker scripts as happily as yours.

A hash blesses exact contents, suiting stable snippets — but any character change breaks the match and re-blocks the script. Automate hash regeneration in your build pipeline so policy updates ship alongside code changes, and prefer external-file versions from vendors where offered — file sources survive snippet edits without policy churn.

Choose per script, not per site: nonces for dynamic, hashes for stable, external files for everything else. Never mix 'unsafe-inline' alongside them (it overrides their protection), and never accept a static hardcoded nonce from a tutorial — a predictable nonce blesses attackers' scripts too. The Python snippet below shows the correct server pattern: fresh randomness per request, header and tag in agreement, no caching of nonced responses.

app/csp.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import secrets
from flask import Flask, make_response, render_template_string

app = Flask(__name__)

@app.get("/checkout")
def checkout():
    nonce = secrets.token_urlsafe(16)  # fresh 128-bit value, every response
    policy = ("default-src 'self'; "
              f"script-src 'self' 'nonce-{nonce}'; "
              "object-src 'none'; base-uri 'self'")
    html = render_template_string(
        "<script nonce='{{ n }}'>initCheckout();</script>", n=nonce)
    resp = make_response(html)
    resp.headers["Content-Security-Policy"] = policy
    resp.headers["Cache-Control"] = "no-store"  # never cache nonces
    return resp
📊 Production Insight
A CDN cached a nonced page for an hour and served one nonce to thousands of visitors. The policy still 'worked,' but any response-reader could reuse the blessed token. Mark nonced responses no-store or move nonces to uncached shells.
🎯 Key Takeaway
Nonce dynamic scripts (fresh per response, never cached); hash stable snippets (exact bytes); external files for the rest. Never unsafe-inline.

Refactoring Inline Code Into External Files

External files are the endgame: code served from your origin (or an approved CDN) needs no per-script blessing, survives policy tightening, and gains caching, versioning, and review as side benefits. The refactor has three mechanical parts. First, move each inline block's body into a .js file served from an allowed origin. Second, replace inline event attributes with addEventListener bindings in that file — onclick='buy()' becomes a listener attached by ID. Third, pass server data via safe channels: data-* attributes, JSON in a non-executing script tag, or dedicated endpoints — never by interpolating values into JS strings, which reintroduces injection.

The refactor pays for itself beyond CSP. External scripts get Content-Type enforcement, subresource integrity (SRI) hashes for CDN copies, and proper code review instead of template archaeology. Third-party widgets deserve the same treatment in reverse: prefer vendor file URLs over vendor snippets, pin them with SRI, and review what each addition executes — every host you add to script-src is code running with your page's privileges.

The JavaScript snippet below shows the canonical handler migration: markup carries data, the external file carries behavior. Apply this pattern mechanically across templates — grep for on* attributes first so none hide — and each converted page permanently leaves the inline-blessing treadmill. Your future policy edits then touch origins, not individual blessings.

static/checkout.js (served from your origin)JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BEFORE (blocked): <button onclick="buyNow('sku-42')">Buy</button>
// AFTER: markup carries data, this file carries behavior.
document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('[data-buy-sku]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const sku = btn.dataset.buySku; // data attribute, never code
      buyNow(sku);
    });
  });
});

async function buyNow(sku) {
  const res = await fetch('/api/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ sku }),
  });
  if (!res.ok) throw new Error(`Checkout failed: ${res.status}`);
  window.location.assign('/receipt');
}
Try it live
📊 Production Insight
A refactor moved code to external files but interpolated usernames into a JS string in the template. Stored XSS fired through the 'safe' external script. Pass data via data attributes or JSON — never string-interpolated code.
🎯 Key Takeaway
Move behavior to external files, bind events with addEventListener, and pass data via attributes or JSON — never interpolated strings.

Report-Only Mode and Reporting Endpoints First

Content-Security-Policy-Report-Only is the rehearsal stage: browsers evaluate the policy, log violations, and block nothing. Deploy your candidate policy in this mode with a reporting endpoint (report-uri for broad compatibility, report-to with the Reporting API where supported), then watch reports for at least two full business cycles — including weekly spikes, monthly jobs, and rare flows like high-risk checkout paths that only trigger for some users. Every legitimate violation is a script you must bless or refactor before enforcing.

Treat the report stream as data to triage, not noise to ignore. Group by violated directive and blocked source: your-own inline blocks need nonces or refactoring, unknown third-party hosts need ownership review (is that marketing tag still wanted?), and data: or blob: violations often trace to specific libraries needing explicit allowances. Volume alerts on the report endpoint catch both deploy regressions and injection attempts probing your policy — a sudden burst of blocked evil-looking sources is threat intelligence, not just breakage.

Keep reports flowing after enforcement, because policies rot: new features add scripts, vendors change snippets, CMS editors paste widgets. The curl check below belongs in CI and monitoring alike — assert the enforcing header is present with the expected directives on every deploy, so a dropped header (CDN misconfiguration, framework upgrade) pages you instead of silently disarming the browser. Monitoring first, enforcing second, monitoring forever.

BASH
1
2
3
4
5
6
7
# Assert the enforcing CSP header survives every deploy
HDR=$(curl -sSI https://app.example.com/checkout | grep -i '^content-security-policy:')
echo "$HDR"
echo "$HDR" | grep -q "script-src" || { echo 'FAIL: script-src missing'; exit 1; }
echo "$HDR" | grep -qi 'unsafe-inline' && { echo 'FAIL: unsafe-inline present'; exit 1; }
echo "$HDR" | grep -q 'object-src .none' || echo 'WARN: object-src not none'
echo 'OK: enforcing header present without unsafe-inline'
📊 Production Insight
A team ran report-only for a month but only sampled 1% of traffic to 'save log costs.' The high-risk fraud script (0.3% of checkouts) never appeared in reports and died at enforcement. Sample rates must cover rare flows.
🎯 Key Takeaway
Rehearse in report-only across full traffic cycles, triage every violation class, and keep header assertions in CI after enforcing.

The Directives That Interact: object-src, base-uri, and More

script-src gets the attention, but a policy is only as strong as its neglected directives. object-src 'none' kills plugin-based execution paths (Flash-era vectors and their modern cousins) that script-src doesn't govern. base-uri 'self' prevents attackers from hijacking relative URLs — without it, injected <base> tags reroute your scripts and forms to evil hosts while your script-src looks pristine. Both belong in every policy from day one; neither breaks legitimate apps when set correctly.

Mind the fallback chains. A missing directive falls back to default-src, so set default-src 'self' (or tighter) as the net beneath everything, then carve explicit exceptions per directive. frame-ancestors controls who may embed your pages (use it instead of the legacy X-Frame-Options where possible). form-action restricts where forms submit — an XSS that can't exfiltrate via your forms is a quieter disaster. connect-src governs fetch and WebSocket targets, containing data theft when markup injection succeeds.

Review third-party additions directive by directive. Each new allowed host in script-src runs code with full page privileges; each connect-src addition is a sanctioned exfiltration channel. Require a business owner and a review date for every third-party entry, and re-validate vendor snippets with hashes on change. A policy with twenty unreviewed marketing hosts isn't defence in depth — it's a guest list nobody checks.

🔥Set the unglamorous directives too
object-src 'none' and base-uri 'self' close execution and hijack paths that script-src doesn't cover. Pair them with a tight default-src fallback on every policy from the first deploy.
📊 Production Insight
An audited policy had a perfect script-src and no base-uri directive. A stored-XSS payload injected a <base> tag and rerouted form posts to a lookalike domain for weeks. Two tokens in the header would have contained it.
🎯 Key Takeaway
Cover object-src, base-uri, form-action, and frame-ancestors alongside script-src, and review every third-party host as privileged code.

Rolling Out CSP Without Breaking Checkout

The safe rollout is a sequence, not a switch: report-only monitoring, violation triage, refactoring with nonces and hashes, funnel alerting, then enforcement during business hours with eyes on the metrics — and an instant rollback path tested before you need it. Each stage has an exit criterion: monitoring is done when two business cycles show only known-legitimate violations; refactoring is done when reports would be empty under enforcement; alerting is done when a simulated header drop pages within minutes.

Make the deploy mechanics boring. Serve the header from application code or instantly-purgeable config, never from slow-propagating edge rules you can't reverse. Stage the rollout by route sensitivity if you can: low-risk content pages first, checkout and payment flows last with extra monitoring. Keep a one-command rollback (previous known-good header) rehearsed and documented — the team that needs it at 2 AM shouldn't be composing it at 2 AM.

After enforcement, keep improving. Tighten directives incrementally (strict-dynamic for modern script loading, trusted types for DOM injection sinks), prune dead third-party hosts quarterly, and review violation reports weekly as both breakage radar and attack telemetry. CSP done right compounds: each quarter the policy gets shorter, the page gets faster (fewer third parties), and the XSS that lands in your tracker becomes a blocked-violation report instead of an incident.

📊 Production Insight
A team enforced on Friday evening 'to have the weekend as buffer.' Weekend traffic patterns missed the B2B admin portal entirely; it broke for every enterprise user Monday morning. Enforce mid-week, mid-day, with the full team present.
🎯 Key Takeaway
Sequence report-only, triage, refactor, alerting, then business-hours enforcement with instant rollback — and keep tightening quarterly.
● Production incidentPOST-MORTEMseverity: high

A New CSP Header Silenced Checkout for 52 Minutes

Symptom
Ten minutes after a 'security hardening' deploy added a strict Content-Security-Policy header, the checkout completion rate fell 94%. The payment widget, address validator, and fraud-fingerprint script — all inline blocks added by different teams over 3 years — were refused by the new script-src 'self' directive. Roughly 1,900 shoppers hit a dead checkout button over 52 minutes, costing an estimated $47,000 in stalled sales. The error was invisible to server monitoring (HTTP 200s everywhere) and appeared only as console violations plus a collapsing business metric.
Assumption
The security team assumed script-src 'self' covered 'our scripts' without realizing 'self' covers script files from your origin — not inline blocks, which need nonces or hashes explicitly. They assumed staging had validated the change, but staging traffic never exercised the third-party fraud script that only loads for high-risk orders in production. And they assumed rollback would be instant, but the header was baked into a CDN edge config with a 40-minute propagation delay.
Root cause
The enforcing header shipped with no report-only phase, so 14 legitimate inline scripts across checkout had never been inventoried. The policy allowed file-based scripts from the origin but blessed nothing inline; every inline block died on arrival. Detection lagged because no alert watched violation reports (none were configured) or the checkout funnel in real time — the business dashboard caught it, 35 minutes in.
Fix
Emergency rollback at the CDN took 52 minutes to propagate fully. The relaunch followed the discipline skipped the first time: 2 weeks in report-only mode with a violation collector, which surfaced all 14 inline scripts plus 3 forgotten marketing tags; refactor of 11 blocks into external files; nonces for the 3 dynamic server-rendered snippets; hashes for 2 stable vendor loaders; funnel alerting on checkout completion; and CDN config changes moved to instant-purgeable snippets instead of slow-propagating edge rules.
Key lesson
  • Never enforce a new CSP without a report-only monitoring phase — violation reports are the inventory of what you'll break.
  • script-src 'self' doesn't cover inline code. Audit every inline block, handler attribute, and javascript: URL before touching the header.
  • Keep security-header deploys instantly reversible: slow CDN propagation turns a 5-minute mistake into a 52-minute outage.
Production debug guideFive steps from silent page to enforced, monitored policy.5 entries
Symptom · 01
Page features die with 'Refused to execute inline script' console errors
→
Fix
Read the violated directive in the console message — it names the exact directive (usually script-src) and the blocked source ('inline'). List every blocked block on the page, then decide per script: refactor into an external file (preferred), bless with a nonce (dynamic server-rendered), or pin with a hash (stable snippet). Don't touch the header until each script has an owner and a plan.
Symptom · 02
You need a quick safe fix for one dynamic inline script
→
Fix
Generate a cryptographically random nonce per response (at least 128 bits, base64), add 'nonce-<value>' to script-src, and emit the identical nonce on the script tag. Verify in the console that the violation clears. Never reuse nonces across responses or cache pages containing them — a reusable nonce is just unsafe-inline with extra steps.
Symptom · 03
A stable third-party snippet (analytics, chat) is blocked
→
Fix
Compute the snippet's SHA-256 hash exactly as served (browsers hash the raw block contents) and add 'sha256-<base64>' to script-src. Re-verify after any vendor or formatting change — one altered character breaks the match. Where the vendor offers an external-file version, prefer it: file sources survive snippet edits without policy updates.
Symptom · 04
Inline event handlers (onclick, onload) and javascript: URLs are blocked
→
Fix
Remove them from markup and bind listeners from your external scripts with addEventListener instead. This is non-negotiable cleanup — handlers can't take nonces individually, and allowing them via unsafe-hashes is a niche exception, not a strategy. Grep templates for 'on[a-z]+=' to find every instance, including those injected by CMS content and legacy jQuery plugins.
Symptom · 05
You're ready to enforce the policy without breaking checkout again
→
Fix
Run report-only mode for at least 2 weeks covering all traffic patterns (including rare flows like high-risk checkout), fix every legitimate violation, add funnel and violation-volume alerting, verify the header deploy path rolls back in minutes, then flip to enforcing during business hours with the team watching the funnel — never on a Friday.
CSP Script Fixes at a Glance
Root CauseHow to ConfirmFixPrevention
Inline block with no nonce or hashConsole names script-src with an 'inline' blocked sourceNonce (dynamic), hash (stable), or external fileInventory inline scripts; forbid new ones in review
Inline event handlers and javascript: URLsViolations on handler attributes; grep finds on* in templatesaddEventListener in external files; data attributes for valuesTemplate linting that rejects inline handlers
Stable vendor snippet changed bytesHash mismatch after vendor edit or reformattingRegenerate hash in build; prefer vendor file URLs with SRIAutomated hash updates shipped with code changes
Policy enforced without monitoringBreakage discovered via business metrics, not reportsRoll back; run report-only phase; add funnel alertsReport-only first always; instant rollback paths
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
rg -l --glob '*.html' -e '<script[^>]*>' templates/ | tee /tmp/inline-pages.txtWhy Your Inline Script Stopped Running
appcsp.pyfrom flask import Flask, make_response, render_template_stringNonce vs Hash
staticcheckout.js (served from your origin)document.addEventListener('DOMContentLoaded', () => {Refactoring Inline Code Into External Files
HDR=$(curl -sSI https://app.example.com/checkout | grep -i '^content-security-po...Report-Only Mode and Reporting Endpoints First

Key takeaways

1
Inline scripts are blocked because browsers can't distinguish them from injected attack code.
2
Bless scripts with per-response nonces (dynamic) or hashes (stable)
never unsafe-inline.
3
Refactor behavior into external files with addEventListener; pass data via attributes or JSON.
4
Rehearse policies in report-only mode across full traffic cycles before enforcing.
5
Set object-src, base-uri, and other sibling directives
script-src alone isn't a policy.
6
Monitor violation reports and header presence continuously; tighten the policy quarterly.

Common mistakes to avoid

5 patterns
×

Adding unsafe-inline to silence violations

Symptom
Console goes quiet while the policy permits every inline script — including injected attack code — to execute.
Fix
Remove unsafe-inline and bless legitimate scripts individually with nonces, hashes, or external files.
×

Reusing or caching nonces across responses

Symptom
One leaked or cached nonce blesses attacker-injected scripts for every visitor who receives it.
Fix
Generate 128+ random bits per response, emit matching header and tag, and mark nonced responses no-store.
×

Hashing a snippet and forgetting vendors edit code

Symptom
A vendor reformats their loader and the hash silently stops matching, breaking the feature with no code change of yours.
Fix
Regenerate hashes in the build pipeline and prefer versioned vendor file URLs with SRI where offered.
×

Enforcing on day one without report-only data

Symptom
Forgotten widgets and rare flows break simultaneously, discovered through collapsing business metrics.
Fix
Run report-only across full business cycles, triage every violation class, then enforce with funnel alerts live.
×

Setting script-src but ignoring base-uri and object-src

Symptom
Injected base tags hijack forms and plugin paths execute despite a perfect script-src directive.
Fix
Ship object-src 'none', base-uri 'self', and a tight default-src fallback in every policy from the start.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why does script-src 'self' still block your inline scripts?
Q02SENIOR
When would you choose a nonce over a hash for an inline script?
Q03SENIOR
Why is unsafe-inline never the right fix for blocked scripts?
Q04SENIOR
How do you roll out a new enforcing policy without breaking production?
Q05SENIOR
An attacker injects markup but your CSP holds. What still needs attentio...
Q01 of 05JUNIOR

Why does script-src 'self' still block your inline scripts?

ANSWER
Because 'self' authorizes script files from your origin, not inline blocks — the browser can't distinguish your inline code from injected code, so inline needs its own proof via nonce or hash regardless of origin.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I deliver CSP in a meta tag instead of a header?
02
Does CSP replace input sanitization and output escaping?
03
What is strict-dynamic and should I use it?
04
Why did my hash stop matching when I changed nothing?
05
How do nonces interact with CDN caching?
06
Should report-uri endpoints be on the same origin?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's XSS. Mark it forged?

6 min read · try the examples if you haven't

←
Previous
Rate Limiting and Credential Stuffing Defence
2 / 2 · XSS