Configuring Content-Security-Policy with SRI
Permalink to "Configuring Content-Security-Policy with SRI"Part of Browser Enforcement & Security Boundaries, this page covers exactly how to align Content-Security-Policy header directives with integrity attributes so the browser enforces both origin allowlisting and payload verification simultaneously.
Quick reference
Permalink to "Quick reference"| Item | Value |
|---|---|
| Header | Content-Security-Policy |
| Relevant directives | script-src, style-src, default-src, script-src-elem, style-src-elem |
| Required tag attribute (cross-origin) | crossorigin="anonymous" |
| Hash algorithm (recommended) | sha384 |
| Fallback / monitoring mode | Content-Security-Policy-Report-Only |
| Browser support (SRI + CSP) | Chrome 45+, Firefox 43+, Safari 11.1+, Edge 17+ |
Why these two controls must be configured together
Permalink to "Why these two controls must be configured together"CSP and SRI defend against different segments of a supply chain attack. CSP restricts which origins the browser will contact at all; SRI verifies that the bytes delivered from those origins match a known-good cryptographic hash. A compromised CDN that still serves assets from an allowed origin defeats CSP alone — the browser receives the tampered file without complaint. SRI catches the mismatch. Omitting either layer leaves a gap:
- CSP without SRI: an attacker who gains write access to an allowed CDN origin can inject arbitrary code.
- SRI without CSP: a cross-site script injection can load an external file from an attacker-controlled origin, bypassing integrity checks entirely.
Together they create a two-factor trust model: the browser checks the source and the content of every external asset. This interaction is part of the broader Core SRI Fundamentals & Browser Security Boundaries enforcement model.
Canonical implementation
Permalink to "Canonical implementation"The following HTML fragment shows the complete, production-ready pattern. Both attributes are required on every cross-origin resource; removing either one degrades the security guarantee.
<!-- External script from a CDN with CSP + SRI -->
<script
src="https://cdn.example.com/libs/framework-3.2.1.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux6EYkFGCO3BNLZ6TDkxJLGfm1yPOD"
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<!-- External stylesheet with CSP + SRI -->
<link
rel="stylesheet"
href="https://cdn.example.com/css/tokens-2.0.0.min.css"
integrity="sha384-AbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEf123456"
crossorigin="anonymous"
/>
The matching CSP header allows exactly those CDN origins and nothing more:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://cdn.example.com;
report-uri /csp-report
crossorigin="anonymous" is not optional. Without it the browser issues a no-cors request and receives an opaque response — a body the browser deliberately withholds from JavaScript — making hash comparison impossible. The resource is blocked regardless of whether the integrity hash is correct.
Variant examples
Permalink to "Variant examples"1. Nonce for server-rendered inline scripts
Permalink to "1. Nonce for server-rendered inline scripts"When your server-side template renders critical inline configuration, a per-request nonce allows that script to execute without using unsafe-inline. Generate a cryptographically random value for each HTTP response:
import secrets, base64
nonce = base64.b64encode(secrets.token_bytes(18)).decode()
# e.g. "rAnd0mBase64StringHere=="
Inject the nonce into both the header and the tag:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0mBase64StringHere==' https://cdn.example.com;
<script nonce="rAnd0mBase64StringHere==">
window.__APP_CONFIG__ = { apiBase: "/api/v2" };
</script>
Nonces and integrity attributes serve separate purposes and can appear on the same tag for inline scripts you hash ahead of time. For external files, integrity alone is the right tool.
2. strict-dynamic for module graph loading
Permalink to "2. strict-dynamic for module graph loading"When a trusted entry-point script dynamically imports further modules (common in ES module architectures), strict-dynamic propagates trust to those child imports without listing each origin explicitly:
Content-Security-Policy:
script-src 'strict-dynamic' 'nonce-rAnd0mBase64StringHere==' https:;
The entry-point script carries the nonce. All scripts it loads programmatically inherit trust. Add integrity checks on the entry point itself for the payload-verification benefit:
<script
type="module"
src="https://cdn.example.com/app/entry.js"
integrity="sha384-Xy1Pq2Rs3Tv4Uw5Vx6Wy7Xz8Ya9Zb0Ac1Bd2Ce3Df4Eg5Fh6Gi7Hj8Ik9Jl0Km"
crossorigin="anonymous"
nonce="rAnd0mBase64StringHere=="
></script>
3. Nginx server-side header injection
Permalink to "3. Nginx server-side header injection"Inject the header at the reverse proxy layer so it applies to every response without touching application code:
server {
listen 443 ssl;
server_name app.example.com;
add_header Content-Security-Policy
"default-src 'self'; \
script-src 'self' https://cdn.example.com; \
style-src 'self' https://cdn.example.com; \
font-src 'self' https://cdn.example.com; \
img-src 'self' data: https://cdn.example.com; \
report-uri /csp-report"
always;
}
The always flag ensures the header is sent on error responses (4xx, 5xx) as well as success responses — without it browsers receive no policy on error pages and can load arbitrary scripts from them.
For edge-layer injection with CDN Trust Mapping & Routing, adapt the same directive set into a Cloudflare Worker Response constructor or an AWS CloudFront response policy.
Gotchas and edge cases
Permalink to "Gotchas and edge cases"-
Missing
crossorigin="anonymous"on external resources. This is the single most common SRI deployment error. An external<script>or<link>withoutcrossorigin="anonymous"fetches in no-cors mode. The browser cannot compare the opaque-response body against theintegrityhash and blocks the resource — even if your CSP header explicitly allows the origin. Always add the attribute; it has no negative effect on same-origin resources. -
Using
unsafe-inlinenegates CSP enforcement for scripts. Theunsafe-inlinekeyword permits any inline<script>block on the page, including attacker-injected ones. It defeats the key purpose of pairing CSP with SRI. Replaceunsafe-inlinewith nonces or inline script hashes (note: inline script hashes are separate from external-fileintegrityattributes and usesha256-...syntax directly in thescript-srcdirective). -
Duplicate CSP headers concatenate unpredictably. If your application server and your reverse proxy both set
Content-Security-Policy, browsers receive two headers. The effective policy is the intersection (the most restrictive of each directive across both headers). This can silently block resources that appear allowed in one header. Centralise header injection at one layer and audit withcurl -Ito confirm a single header is returned. -
Hardcoding hashes in static templates breaks on every asset update. An asset update changes the hash. If you embed the
integrityvalue by hand, each deployment to a new asset version requires a manual template edit. Automate hash injection via Automating Hash Generation in Webpack 5 or equivalent build tooling so the attribute is always in sync with the bytes on disk. -
require-sri-foris not yet broadly supported. Therequire-sri-for scriptdirective would instruct the browser to reject any external script lacking an integrity attribute. As of mid-2026 it is still an experimental directive removed from the CSP Level 3 spec draft pending redesign. Do not rely on it for production enforcement; useintegrityattributes explicitly on every external tag instead. -
Report-URI vs Report-To endpoint.
report-urisends aPOSTto a single URL and is universally supported.report-touses the newer Reporting API and requires aReporting-Endpointsresponse header to define endpoint groups. Deployreport-urifirst; addreport-tolater when you have the Reporting API infrastructure in place. For Graceful Fallback Strategies to work, you must have violation data flowing before cutover to enforcement mode.
Verification steps
Permalink to "Verification steps"1. Confirm the header in a real response
Permalink to "1. Confirm the header in a real response"curl -sI https://app.example.com/ | grep -i content-security-policy
Expected output — a single Content-Security-Policy header with your directives. A second header on the next line indicates a duplication problem.
2. Confirm integrity attributes appear in built HTML
Permalink to "2. Confirm integrity attributes appear in built HTML"grep -E 'integrity="sha384-' dist/index.html
Every external <script> and <link rel="stylesheet"> should appear in the output. A missing line means your build tooling is not injecting hashes.
3. Trigger a deliberate mismatch in a staging environment
Permalink to "3. Trigger a deliberate mismatch in a staging environment"Change one character of an integrity attribute value in a staging HTML file, open the page in Chrome DevTools, and confirm the console shows:
Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.example.com/libs/framework-3.2.1.min.js' with computed
SHA-384 integrity 'oqVuAfXRKap7fdg...'. The resource has been blocked.
If you do not see this message, the crossorigin attribute is likely missing, or the resource is served same-origin (which does not require CORS).
4. Run in Report-Only mode before enforcement
Permalink to "4. Run in Report-Only mode before enforcement"Deploy with Content-Security-Policy-Report-Only and the same directive set for at least 72 hours. Aggregate violation reports at your report-uri endpoint. Any blocked-uri entries in the violation payloads indicate origins or inline scripts not yet accounted for in your policy. Address every violation before switching to Content-Security-Policy.
5. CI gate: fail the build on missing integrity attributes
Permalink to "5. CI gate: fail the build on missing integrity attributes"# Fail CI if any external script or stylesheet link lacks an integrity attribute
if grep -E '<(script|link)[^>]+src=|href=' dist/index.html | grep -v 'integrity='; then
echo "ERROR: external resource missing integrity attribute" >&2
exit 1
fi
Add this check to your deployment pipeline alongside the Understanding Cryptographic Hash Algorithms validation step to catch regressions before production.
Related
Permalink to "Related"- Browser Enforcement & Security Boundaries — the parent guide: fetch lifecycle mechanics, CI/CD gating, and cache invalidation workflows
- Graceful Fallback Strategies — handling SRI failures with onerror handlers and
Report-Onlyrollback procedures - Implementing Dynamic Script Loaders with Integrity — how to attach integrity checks to programmatically injected scripts where a static
<script>tag is not possible