Subresource Integrity (SRI)
Verify integrity of third-party resources with SRI
Subresource Integrity (SRI)
Subresource Integrity (SRI) ensures third-party scripts and styles haven't been tampered with. It's essential when loading resources from CDNs.
The Problem
<!-- What if CDN is compromised? -->
<script src="https://cdn.example.com/library.js"></script>
<!-- Attacker could inject malicious code! -->The Solution: SRI
<!-- Verify file hasn't changed -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- If hash doesn't match, browser blocks it! -->How SRI Works
1. Download resource from CDN
2. Calculate hash (SHA-256, SHA-384, or SHA-512)
3. Compare with integrity attribute
4. If match: Execute
If mismatch: Block and throw errorGenerating SRI Hashes
Command Line
# SHA-384 (recommended)
openssl dgst -sha384 -binary library.js | openssl base64 -A
# SHA-512 (strongest)
openssl dgst -sha512 -binary library.js | openssl base64 -A
# Or with curl + openssl
curl https://cdn.example.com/library.js | \
openssl dgst -sha384 -binary | \
openssl base64 -ANode.js
import crypto from 'crypto';
import fs from 'fs';
function generateSRI(filePath: string, algorithm: 'sha256' | 'sha384' | 'sha512' = 'sha384'): string {
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto
.createHash(algorithm)
.update(fileBuffer)
.digest('base64');
return `${algorithm}-${hash}`;
}
// Usage
const integrity = generateSRI('./library.js', 'sha384');
console.log(integrity);
// sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wCOnline Tools
https://www.srihash.org/
Paste URL, get SRI hashImplementation
Scripts
<script
src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"
integrity="sha512-qlzIeUtTg7eBpmEaS12NZgxz52YYZVF5myj89mjJEesBd/oE9UPsYOX2QAXzvOAZYEvQohKdcY8vQWY3CiFuQA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>Stylesheets
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
/>Next.js
import Script from 'next/script';
export default function Page() {
return (
<>
<Script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
</>
);
}React
import { useEffect } from 'react';
function LoadExternalScript() {
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://cdn.example.com/library.js';
script.integrity = 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC';
script.crossOrigin = 'anonymous';
script.onerror = () => {
console.error('SRI verification failed!');
};
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
return null;
}Multiple Hashes (Fallback)
Provide multiple hashes for backwards compatibility:
<script
src="https://cdn.example.com/library.js"
integrity="
sha512-Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==
sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
"
crossorigin="anonymous"
></script>
<!-- Browser uses strongest algorithm it supports -->crossorigin Attribute
Required for SRI!
<!-- ❌ Won't work - missing crossorigin -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-..."
></script>
<!-- ✅ Correct -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>crossorigin Values
<!-- anonymous: No credentials sent -->
<script src="..." integrity="..." crossorigin="anonymous"></script>
<!-- use-credentials: Send credentials (cookies, auth) -->
<script src="..." integrity="..." crossorigin="use-credentials"></script>Automating SRI
Webpack Plugin
npm install webpack-subresource-integrity// webpack.config.js
const SriPlugin = require('webpack-subresource-integrity');
module.exports = {
output: {
crossOriginLoading: 'anonymous',
},
plugins: [
new SriPlugin({
hashFuncNames: ['sha256', 'sha384'],
enabled: process.env.NODE_ENV === 'production',
}),
],
};Vite Plugin
npm install vite-plugin-sri// vite.config.ts
import { defineConfig } from 'vite';
import sri from 'vite-plugin-sri';
export default defineConfig({
plugins: [
sri({
algorithms: ['sha384'],
}),
],
});Build Script
// scripts/generate-sri.ts
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import glob from 'glob';
const files = glob.sync('dist/**/*.{js,css}');
files.forEach(file => {
const content = fs.readFileSync(file);
const hash = crypto
.createHash('sha384')
.update(content)
.digest('base64');
console.log(`${path.basename(file)}: sha384-${hash}`);
});CSP Integration
Combine SRI with Content Security Policy:
const csp = `
script-src 'self' https://cdn.example.com;
require-sri-for script style;
`;
response.headers.set('Content-Security-Policy', csp);Note: require-sri-for is deprecated. Use CSP with strict allowlisting instead.
Error Handling
// Detect SRI failures
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLScriptElement) {
console.error('Script failed to load:', event.target.src);
console.error('Possible SRI verification failure');
// Load fallback or show error to user
loadFallbackScript();
}
}, true);
function loadFallbackScript() {
const script = document.createElement('script');
script.src = '/fallback/library.js'; // Local fallback
document.body.appendChild(script);
}Fallback Strategy
function ScriptWithFallback() {
const [useFallback, setUseFallback] = useState(false);
const handleError = () => {
console.warn('CDN failed, using fallback');
setUseFallback(true);
};
if (useFallback) {
return <script src="/local/library.js" />;
}
return (
<script
src="https://cdn.example.com/library.js"
integrity="sha384-..."
crossOrigin="anonymous"
onError={handleError}
/>
);
}Dynamic Imports with SRI
// For dynamically imported modules
async function loadModule() {
try {
const module = await import(
/* webpackChunkName: "dynamic" */
'./dynamic-module.js'
);
return module;
} catch (error) {
console.error('Module failed SRI check', error);
// Load fallback
}
}Testing SRI
Manual Test
# 1. Get current hash
curl https://cdn.example.com/library.js | \
openssl dgst -sha384 -binary | \
openssl base64 -A
# 2. Compare with integrity attribute
# 3. If they match, SRI is workingAutomated Test
describe('SRI', () => {
it('should have integrity attribute on CDN scripts', () => {
const scripts = document.querySelectorAll('script[src^="https://cdn"]');
scripts.forEach(script => {
expect(script.getAttribute('integrity')).toBeTruthy();
expect(script.getAttribute('crossorigin')).toBe('anonymous');
});
});
});Playwright Test
import { test, expect } from '@playwright/test';
test('CDN resources have SRI', async ({ page }) => {
await page.goto('/');
const scripts = await page.$$eval(
'script[src^="https://"]',
scripts => scripts.map(s => ({
src: s.src,
integrity: s.integrity,
crossOrigin: s.crossOrigin,
}))
);
scripts.forEach(script => {
expect(script.integrity).toBeTruthy();
expect(script.crossOrigin).toBe('anonymous');
});
});Best Practices
- Always use SRI for CDN resources: Third-party scripts/styles
- Use SHA-384 or SHA-512: More secure than SHA-256
- Include crossorigin: Required for SRI to work
- Automate generation: Build-time SRI generation
- Update hashes: When updating library versions
- Test in staging: Ensure SRI doesn't break site
- Fallback strategy: Local copies for critical resources
- Monitor failures: Track SRI errors in production
- Document exceptions: Why some resources don't use SRI
- CSP integration: Combine with Content Security Policy
When to Use SRI
✅ Use SRI:
- Third-party CDN scripts (jQuery, React, etc.)
- Third-party stylesheets (Bootstrap, fonts)
- Analytics scripts
- Any external resource you don't control
❌ Don't need SRI:
- Resources from same origin (
'self') - Resources you fully control
- Frequently changing resources (API responses)
Common Issues
Issue: Hash Mismatch
Possible causes:
1. Library was updated (new version)
2. CDN was compromised (rare)
3. Wrong hash algorithm
4. Missing crossorigin attributeSolution: Regenerate hash for new version
Issue: CORS Error
<!-- ❌ Missing crossorigin -->
<script src="https://cdn.example.com/lib.js" integrity="sha384-..."></script>
<!-- ✅ Add crossorigin -->
<script src="https://cdn.example.com/lib.js" integrity="sha384-..." crossorigin="anonymous"></script>Issue: Old Browser Support
<!-- Fallback for browsers without SRI support -->
<script>
if (!('integrity' in document.createElement('script'))) {
// Load from trusted source or show warning
console.warn('Browser does not support SRI');
}
</script>Common Pitfalls
❌ No SRI on CDN scripts: Vulnerable to tampering
✅ Always use SRI for third-parties
❌ Missing crossorigin: SRI won't work
✅ Include crossorigin="anonymous"
❌ Manual hash management: Error-prone
✅ Automate with build tools
❌ No fallback: Site breaks if CDN fails
✅ Have local fallback copies
❌ Ignoring SRI errors: Silent failures
✅ Monitor and alert on SRI failures
SRI is your defense against CDN compromise—use it for all third-party resources!