DEV Community

Hamza Ezzaydia
Hamza Ezzaydia

Posted on

I built what browsers refused to ship: SRI for fetch()

The Problem Nobody Talks About

Browser SRI (Subresource Integrity) has been around since 2016. It lets you verify that scripts loaded from CDNs haven't been tampered with:

<script src="https://cdn.example.com/lib.js"
        integrity="sha256-abc123..."
        crossorigin="anonymous"></script>
Enter fullscreen mode Exit fullscreen mode

Here's the thing: SRI only works on <script> and <link> tags.

When you fetch() a file - a WASM module, an AI model, a JSON config, any binary data - you get zero integrity protection. The browser just... trusts it.

What Happens When a CDN Gets Compromised

In June 2024, polyfill.io was compromised. The CDN started serving malicious JavaScript to over 100 million websites.

Sites that used SRI on their script tags were protected. Sites that loaded polyfill.io via fetch() or dynamic import? Completely vulnerable.

This isn't a theoretical attack. It happened. And it will happen again.

Why Native Solutions Fail

You might think: "I'll just hash the response myself with crypto.subtle.digest()"

Try that with a 4GB AI model:

File Size Native crypto.subtle VerifyFetch
100 MB Works Works
1 GB Slow, RAM spike 2MB memory
4 GB Browser crash 2MB memory

Native crypto buffers the entire file into memory before hashing. That's fine for small files, but completely unusable for large ones.

The Solution: VerifyFetch

I built VerifyFetch to fill this gap:

import { verifyFetch } from 'verifyfetch';

const response = await verifyFetch('/model.bin', {
  sri: 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek='
});
Enter fullscreen mode Exit fullscreen mode

If the hash doesn't match, it throws. Your users are protected.

How It Works

VerifyFetch uses a WASM streaming hasher compiled from Rust. Data flows through chunk by chunk - the file is never buffered entirely in memory.

The result: constant ~2MB memory usage whether you're verifying a 10MB file or a 10GB file.

It also includes:

  • Fallback URLs: If verification fails, automatically try a backup server
  • Progress tracking: Know exactly how much has been verified
  • Manifest mode: One JSON file with all your hashes
  • CLI for CI/CD: Fail builds if files change after signing

Generate Hashes

npx verifyfetch sign ./public/*.wasm ./models/*.bin
Enter fullscreen mode Exit fullscreen mode

This creates a vf.manifest.json with all the SRI hashes.

Use in Your App

const vf = await createVerifyFetcher({
  manifestUrl: '/vf.manifest.json'
});

// Hashes are looked up automatically
const model = await vf.arrayBuffer('/model.bin');
const config = await vf.json('/config.json');
Enter fullscreen mode Exit fullscreen mode

Enforce in CI

npx verifyfetch enforce --manifest ./public/vf.manifest.json
Enter fullscreen mode Exit fullscreen mode

Your deploy fails if any file has changed since signing.

Try It

npm install verifyfetch
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/hamzaydia/verifyfetch
Docs: https://verifyfetch.com

I'd love feedback on the API design and any edge cases I might be missing.

Top comments (0)