Adding Integrity to Google Fonts and CSS
Permalink to "Adding Integrity to Google Fonts and CSS"Part of Applying SRI to Stylesheets & Web Fonts, this page is the copy-pasteable reference for putting a valid integrity attribute on a <link rel="stylesheet"> — for both self-hosted CSS and Google Fonts — and for the traps that make stylesheet SRI fail in ways script SRI never does.
Quick Reference
Permalink to "Quick Reference"| Property | Value |
|---|---|
| Element | <link rel="stylesheet"> |
| Attribute | integrity |
| Algorithm identifier | sha384 (default), sha256, sha512 |
| Token format | sha384-<base64 digest> |
| Required companion attribute | crossorigin="anonymous" (cross-origin CSS) |
| CORS response header needed | Access-Control-Allow-Origin |
| Preload support | <link rel="preload" as="style" integrity="…"> |
| CSP enforcement | require-sri-for style |
| Browser support | All major engines (Chromium, Firefox, Safari) |
| Covers font files? | No — @font-face url() is unverified |
Use SHA-384 unless a specific constraint forces another algorithm. The stylesheet URL must return identical bytes on every request, or the hash cannot validate.
How Stylesheet Integrity Works
Permalink to "How Stylesheet Integrity Works"The browser fetches the CSS, decodes any Content-Encoding, computes the digest over the decoded bytes, and compares it to the integrity token before parsing the stylesheet into the CSSOM. A match applies the styles; a mismatch blocks the stylesheet and logs net::ERR_SRI_FAILED. The guarantee is scoped to the CSS bytes only — fonts and images the CSS references through url() are fetched separately and are not covered, a boundary detailed in the parent page and enforced by the same fetch-time gate described in Browser Enforcement & Security Boundaries.
Two conditions must both hold for the check to even run. First, the response body must be readable: for a cross-origin stylesheet that means a CORS-eligible fetch (crossorigin="anonymous") answered with an Access-Control-Allow-Origin header. Second, the bytes must be deterministic — the same URL must return the same payload on every request, or the fixed token in the HTML cannot match what an arbitrary visitor receives. Script SRI usually satisfies both trivially because bundles are content-addressed and static. Stylesheet SRI for fonts breaks precisely because the popular delivery model — the Google Fonts CSS endpoint — violates the second condition, returning per-browser payloads. Everything below is about restoring determinism and keeping the response readable.
Canonical Example
Permalink to "Canonical Example"The following is a complete, production-ready self-hosted stylesheet reference with a SHA-384 integrity attribute. This is the pattern to reach for by default.
<link
rel="stylesheet"
href="https://assets.example.com/fonts/inter.7f3a91.css"
integrity="sha384-h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH"
crossorigin="anonymous"
/>
Generate the sha384-… token from the exact bytes the server delivers:
# Hash the deployed CSS artifact, not the source
openssl dgst -sha384 -binary dist/fonts/inter.css | openssl base64 -A
# → h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH
The -binary flag emits raw digest bytes; -A suppresses newline wrapping. Omitting either yields a value the browser rejects.
Variant Examples
Permalink to "Variant Examples"Pinned Google Fonts CSS version
Permalink to "Pinned Google Fonts CSS version"If you must reference Google’s CSS directly rather than self-hosting, pin a fixed User-Agent fetch and hash that payload — accepting that the token is only valid for browsers receiving the same bytes. This is fragile; self-hosting is strongly preferred.
# Capture one specific representation and hash it
curl -sL \
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
"https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" \
-o inter-pinned.css
openssl dgst -sha384 -binary inter-pinned.css | openssl base64 -A
<!-- Fragile: only validates for User-Agents that receive identical bytes -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
integrity="sha384-Kx9pL2mQ7rT4vW1yB6nE8sH0jC5dF3gA9uZ2xR7wN4tM1oP6qS8bV0cX3yG5hJ2"
crossorigin="anonymous"
/>
Preloading a hashed stylesheet
Permalink to "Preloading a hashed stylesheet"Preload the stylesheet to start the fetch earlier while keeping the hash check. The crossorigin and integrity values must match on both tags, or the preload is discarded and the file is fetched twice.
<link
rel="preload"
as="style"
href="https://assets.example.com/fonts/inter.7f3a91.css"
integrity="sha384-h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://assets.example.com/fonts/inter.7f3a91.css"
integrity="sha384-h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH"
crossorigin="anonymous"
/>
Multiple stylesheets
Permalink to "Multiple stylesheets"Each <link> carries its own token; there is no shared attribute. Compute one digest per file.
<link rel="stylesheet" href="/css/base.a1b2c3.css"
integrity="sha384-Tx0aQ9wE2rY7uI4oP1sD6fG3hJ8kL5mN0bV7cX2zA9qW4eR1tY6uI3oP8sD5fG2"
crossorigin="anonymous" />
<link rel="stylesheet" href="/css/theme.d4e5f6.css"
integrity="sha384-Ry1bS0xF3tZ8vJ5pQ2eE7gH4iK9lM6nO1cW8dY3aB0rX5fS2uZ7vJ4qR9tY6uI3"
crossorigin="anonymous" />
SHA-512 for high-assurance pages
Permalink to "SHA-512 for high-assurance pages"SHA-384 is the default. Use SHA-512 only when a higher-assurance regime calls for it — the token is longer but the tag is otherwise identical. Show SHA-256 only where a legacy constraint forces the shorter digest.
openssl dgst -sha512 -binary dist/fonts/inter.css | openssl base64 -A
<link
rel="stylesheet"
href="https://assets.example.com/fonts/inter.7f3a91.css"
integrity="sha512-9Ck0m1sX3hV6dQ2yB8nE4tR7uP0jC5dF3gA9uZ2xR7wN4tM1oP6qS8bV0cX3yG5hJ2kL5mN0bV7cX2zA9qW4eR1tY6uI3oP8sD5fG2hJ8kL5mN0bV7cX2zA9qW4eR1tY6"
crossorigin="anonymous"
/>
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
Omitting
crossorigin="anonymous"silently blocks the stylesheet. On a cross-origin<link rel="stylesheet">with anintegrityattribute but nocrossorigin, the browser issues a no-CORS opaque fetch whose bytes it cannot read. It cannot hash an unreadable body, so it blocks the resource — not because the hash failed, but because the body was never available. This is the single most common stylesheet-SRI bug; always pairintegritywithcrossorigin="anonymous"for cross-origin CSS. -
The Google Fonts CSS is byte-unstable per browser.
fonts.googleapis.com/css2serves different@font-facerules (formats,unicode-rangesubsets) depending on the requestingUser-Agent. A token hashed from Chrome’s response fails for Firefox or Safari visitors. Do not pin SRI to the live endpoint — self-host the CSS so every visitor receives byte-identical content and one hash validates universally. -
@font-facefont files are never integrity-checked. The stylesheet hash covers the CSS bytes only. Font files named insrc: url(...)are separate subresources with no integrity descriptor in CSS. Self-host and content-address the fonts and restrictfont-srcin your CSP; there is no per-font hash to add. -
Hash the served artifact, not the source. Minification and autoprefixer change the bytes. Compute the digest after the full build, or by
curl-ing the live URL, otherwise every deploy mismatches. -
crossoriginmust match between preload and stylesheet. A mismatch (one omitted, oneanonymous) makes the browser discard the preload and fetch the CSS a second time, silently negating the optimization. -
Compression is transparent, mutation is not. The browser hashes the decoded body, so
Content-Encoding: gziporbrfrom the CDN is fine. What breaks the token is CDN-side CSS minification, URL rewriting, or comment stripping applied at the edge — any transformation that changes the decoded bytes. Disable payload-mutating rules for SRI-protected stylesheet paths. -
@importinside a hashed stylesheet escapes the check. An@import url(...)pulls a second stylesheet that carries no integrity check of its own. Inline the imported rules into the hashed file, or serve the sub-stylesheet from'self'under astyle-srcrestriction.
Verification Steps
Permalink to "Verification Steps"1. Confirm both attributes are present
Permalink to "1. Confirm both attributes are present"grep -E 'rel="stylesheet"' dist/index.html | grep -E 'integrity=|crossorigin='
Expected — every cross-origin stylesheet line shows both integrity="sha384-…" and crossorigin="anonymous".
2. Re-derive the hash from the live CSS
Permalink to "2. Re-derive the hash from the live CSS"curl -sL https://assets.example.com/fonts/inter.7f3a91.css \
| openssl dgst -sha384 -binary | openssl base64 -A
The output must match the token in the HTML character for character.
3. Confirm clean enforcement in the browser
Permalink to "3. Confirm clean enforcement in the browser"Load the page in Chrome or Firefox with DevTools open. A failure produces:
Failed to find a valid digest in the 'integrity' attribute for resource
'https://assets.example.com/fonts/inter.7f3a91.css' with computed SHA-384
integrity 'sha384-<actual>'. The resource has been blocked.
No such message, and a styled render, means the hash is correct. Flip one character of the token and reload to prove the gate blocks a mismatch, then restore it.
4. Gate CI on stylesheet hash drift
Permalink to "4. Gate CI on stylesheet hash drift"Add a pipeline step that re-derives every stylesheet token from the built files and diffs it against the emitted HTML, failing the build on any divergence so a stale hash never ships:
# GitHub Actions
- name: Verify stylesheet SRI tokens
run: |
grep -oP 'href="\K[^"]+\.css' dist/index.html | while read -r css; do
declared=$(grep -oP "href=\"${css//\//\\/}\"[^>]*integrity=\"\K[^\"]+" dist/index.html)
computed="sha384-$(openssl dgst -sha384 -binary "dist/${css}" | openssl base64 -A)"
[ "$declared" = "$computed" ] || { echo "Mismatch: $css"; exit 1; }
done
echo "All stylesheet hashes verified."
The step exits non-zero on drift, blocking promotion before a broken token reaches a CDN edge.
Related
Permalink to "Related"- Applying SRI to Stylesheets & Web Fonts — the full workflow, the coverage boundary, and how to handle font files
- Browser Enforcement & Security Boundaries — the fetch-time gate and
require-sri-for styledirective - Configuring Content Security Policy with SRI — pairing
style-srcandfont-srcwith SRI