When loading scripts, stylesheets, fonts, or images from third-party CDNs or external origins, verify their cryptographic integrity with Subresource Integrity (integrity in HTML and integrity() in CSS url()), configure cross-origin credential handling (crossorigin and cross-origin()), and restrict outgoing Referer headers (referrerpolicy and referrer-policy()). For document-level Content Security Policy and outbound connection allowlists, see Web Security and Restrict outbound network connections with Connection Allowlists.
1. Generate cryptographic integrity digests for immutable assets
Subresource Integrity (SRI) ensures the browser only executes or renders an external asset if its bytes match the cryptographic hash declared by your application.
- Compute digests at build or release time, never from live untrusted responses: Generate integrity hashes during your build or release step from known-good local asset bytes (or copy the official digest published by the package author) and hardcode them into your HTML, CSS, or JavaScript. Never fetch a third-party URL in the browser at runtime to compute its own
integrityhash, which trusts whatever bytes the remote server returns and defeats SRI. - Use
sha384-by default: Format integrity metadata as<algorithm>-<base64-digest>usingsha384-(recommended default),sha512-, orsha256-. - Pin external subresources to immutable, versioned URLs: Always reference third-party assets using immutable, version-pinned or content-addressed URLs (such as
https://cdn.example.com/ui@2.4.0/app.min.js) rather than mutable floating URLs (latest/app.min.js), which expose your site to silent supply-chain changes and break SRI as soon as the remote file updates. If you must load a mutable third-party script that updates in place (such as a vendor analytics tag), omitintegrityon that URL and restrict it with Content Security Policy (strict-dynamic/ nonces) or iframe sandboxing instead. - Space-separate multiple digests when an endpoint may serve multiple valid payloads: All browsers that support SRI implement
sha256-,sha384-, andsha512-. If a URL may legitimately return one of several known-good payloads (such as during an A/B test, staged CDN rollout, or content-negotiated build), list multiple digests of the same algorithm separated by whitespace (for example,integrity="sha384-variantA... sha384-variantB..."). The browser accepts the response if it matches any listed digest of the strongest algorithm present. - Serve cross-origin subresources with CORS headers: Browsers can only verify the integrity of a cross-origin response when it is CORS-readable (
Access-Control-Allow-Origin: *for anonymous requests, or the exact requesting origin withAccess-Control-Allow-Credentials: truefor credentialed requests).
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
const rawBytes =
typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
String.fromCharCode(b),
).join('');
return `sha384-${btoa(binary)}`;
}
2. Secure HTML scripts, stylesheets, and preloads
Set a document-level Referrer-Policy: strict-origin-when-cross-origin HTTP response header (or include <meta name="referrer" content="strict-origin-when-cross-origin"> in <head>) as a baseline default so any subresource or CSS url() that omits a per-request referrer modifier still strips sensitive path and query parameters on cross-origin requests.
Then, on external <script>, <link rel="stylesheet">, <link rel="preload">, and <link rel="modulepreload"> elements, always set integrity, crossorigin, and referrerpolicy together:
integrity="sha384-...": Blocks execution or application of the resource if the response bytes do not match the expected digest (for a blocked<link rel="stylesheet">,linkElement.sheetremainsnulland anerrorevent fires).crossorigin="anonymous"(recommended default for public CDN assets) orcrossorigin="use-credentials": Required wheneverintegrityis set on a cross-origin resource. Omittingcrossorigincauses the browser to fetch the resource inno-corsmode (producing an opaque response that cannot be hash-verified) and block the resource.referrerpolicy="no-referrer"(recommended default for third-party CDNs) orreferrerpolicy="strict-origin-when-cross-origin": Overrides the document referrer policy to prevent leaking any origin or path information in theRefererrequest header to the third-party host.
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />
<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
rel="stylesheet"
href="https://cdn.example.com/ui@2.4.0/styles.css"
integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
When fetching external subresources programmatically with fetch(), pass the matching integrity, mode, credentials, and referrerPolicy options (a digest mismatch rejects the promise with TypeError):
const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
mode: 'cors',
credentials: 'omit',
referrerPolicy: 'no-referrer',
});
3. Secure CSS images, fonts, and imported stylesheets with url() modifiers
CSS url() request modifiers (cross-origin(), referrer-policy(), and integrity()) control the fetch mode, referrer header, and Subresource Integrity verification of CSS-loaded resources—including background-image, content, mask-image, border-image-source, filter, @font-face src, and @import rules—directly from CSS:
| Modifier function | Argument syntax & allowed values | Purpose |
|---|---|---|
cross-origin(<ident>) |
Unquoted keyword: anonymous (recommended default) or use-credentials |
Fetches the CSS resource using CORS and controls whether credentials are sent. Always include alongside integrity() on cross-origin CSS resources. |
referrer-policy(<ident>) |
Unquoted keyword: no-referrer (recommended default for third-party assets), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade, or unsafe-url |
Overrides the document referrer policy for this specific CSS request. |
integrity("<string>") |
Quoted string: "sha384-<base64>" (or space-separated digests "sha384-... sha384-...") |
Verifies the fetched CSS resource against the expected cryptographic digest before rendering or applying it. |
Follow these CSS syntax and security rules when authoring url() modifiers:
- Quote the URL string inside
url("..."): Request modifiers are only valid when the URL insideurl(...)is a quoted<string>(unquoted URLs cannot use modifiers). - Specify each modifier type at most once per
url(): You can combinecross-origin(),referrer-policy(), andintegrity()in any order insideurl("..." ...), but repeating the same modifier type twice (such as twointegrity()modifiers) is invalid. To supply multiple hashes, space-separate them inside a singleintegrity("sha384-... sha384-...")string. - Always pair cross-origin
integrity()withcross-origin(anonymous): While@font-facesrcandmask-imagealready fetch in anonymouscorsmode by default,background-image,content,border-image-source, and@importdefault tono-corsmode (producing an opaque response that cannot be hash-verified) and external SVGfilterdefaults tosame-originmode (blocking cross-origin URLs outright). Includingcross-origin(anonymous)(orcross-origin(use-credentials)) ensures consistent CORS verification across every CSS property. - Place modifiers inside
url("...")in@importand@font-facerules: Keepcross-origin(),referrer-policy(), andintegrity()insideurl("..." ...), while keeping font descriptors likeformat("woff2")andtech(...)outsideurl(...).
@import url(
"https://cdn.example.com/theme@1.2.0/tokens.css"
cross-origin(anonymous)
referrer-policy(no-referrer)
integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);
@font-face {
font-family: "BrandSans";
src: url(
"https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
cross-origin(anonymous)
referrer-policy(no-referrer)
integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
) format("woff2");
font-display: swap;
}
.verified-graphic {
background-image: url(
"https://cdn.example.com/media@1.0.0/badge.svg"
cross-origin(anonymous)
referrer-policy(no-referrer)
integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
);
}
Fallback strategies
If your Baseline target does not yet support CSS url() request modifiers (cross-origin(), referrer-policy(), or integrity()), combine document-level policies with cascading CSS declarations so subresources remain functional and protected:
- Prefer HTML
<link rel="stylesheet">over CSS@importwhen cross-browser SRI is required: Becausesubresource-integrityandreferrer-policyon<link rel="stylesheet">are widely available across all browsers, load external stylesheets with<link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer">instead of@importwhenever integrity verification must be enforced in every browser. - Rely on the document-level
Referrer-Policyheader or<meta name="referrer">from Step 2: In browsers that do not supportreferrer-policy()inside CSSurl(), the document-levelReferrer-Policy: strict-origin-when-cross-originheader or<meta name="referrer" content="strict-origin-when-cross-origin">element automatically strips sensitive path and query parameters on cross-origin CSS requests. Cascade separate CSS declarations for progressive enhancement: Browsers that do not recognize a modifier inside
url("..." ...)discard the entire declaration at CSS parse time. Declare a plainurl("...")first, followed by across-origin()andreferrer-policy()declaration (for browsers that support those two modifiers but notintegrity()), followed by the full declaration includingintegrity("sha384-..."):- Never put an unverified
url()inside the same comma-separated@font-facesrclist: Writingsrc: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2");in a singlesrcdescriptor bypasses Subresource Integrity at runtime—if a CDN serves a tampered font that fails theintegrity()check, the browser treats it as a load failure and automatically falls back to the second comma-separatedurl("font.woff2"). Using separatesrc:declarations ensures supporting browsers overwrite the unverified declaration at CSS parse time and block tampered fonts at runtime.
@font-face { font-family: "BrandSans"; /* 1. Parse-time fallback for browsers without url() modifier support */ src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2"); /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */ src: url( "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2" cross-origin(anonymous) referrer-policy(no-referrer) ) format("woff2"); /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */ src: url( "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC") ) format("woff2"); font-display: swap; } .verified-graphic { background-image: url("https://cdn.example.com/media@1.0.0/badge.svg"); background-image: url( "https://cdn.example.com/media@1.0.0/badge.svg" cross-origin(anonymous) referrer-policy(no-referrer) ); background-image: url( "https://cdn.example.com/media@1.0.0/badge.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91") ); }- Never put an unverified
Feature-detect CSS
url()modifier support in JavaScript withCSS.supports():
export function getCssUrlModifierSupport() {
const supportsCrossOriginAndReferrer =
typeof CSS !== 'undefined' &&
typeof CSS.supports === 'function' &&
CSS.supports(
'background-image',
'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
);
const supportsUrlIntegrity =
typeof CSS !== 'undefined' &&
typeof CSS.supports === 'function' &&
CSS.supports(
'background-image',
'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
);
return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}