Front-end Engineering Lab

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 error

Generating 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 -A

Node.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/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC

Online Tools

https://www.srihash.org/
Paste URL, get SRI hash

Implementation

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 working

Automated 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

  1. Always use SRI for CDN resources: Third-party scripts/styles
  2. Use SHA-384 or SHA-512: More secure than SHA-256
  3. Include crossorigin: Required for SRI to work
  4. Automate generation: Build-time SRI generation
  5. Update hashes: When updating library versions
  6. Test in staging: Ensure SRI doesn't break site
  7. Fallback strategy: Local copies for critical resources
  8. Monitor failures: Track SRI errors in production
  9. Document exceptions: Why some resources don't use SRI
  10. 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 attribute

Solution: 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!

On this page