WSS
BlogSecurityArchitecturePublished

Subresource Integrity (SRI) in a Dynamically Imported World

Securing static script tags is easy. Securing a modern JavaScript application that heavily relies on dynamic import() and third-party vendors is significantly harder. Here is how to handle CSP and SRI hashes alongside JavaScript Import Maps.

Subresource Integrity (SRI) is one of the most critical security features of the modern web platform. It prevents supply chain attacks. If you load a script from a third-party CDN, and that CDN is compromised by an attacker who injects a crypto-miner into the JavaScript file, SRI stops the script from executing.

In the old world of static HTML, SRI was trivial to implement. You generated a SHA-256 hash of the file during your build process and appended it to the <script> tag:

<script 
  src="https://cdn.example.com/vendor.js" 
  integrity="sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" 
  crossorigin="anonymous"
></script>

When the browser downloads the file, it hashes the contents. If the hash doesn’t exactly match the integrity attribute, the browser throws a security error and refuses to execute the code.

This is perfect for static bundles. But modern web development rarely looks like this anymore.

The Dynamic Import Problem

Today, we aggressively code-split our applications. We heavily rely on dynamic imports (await import('./heavy-module.js')) to defer loading massive dependencies until the user actually interacts with a feature.

This creates a massive security hole.

You cannot attach an integrity attribute to a dynamic import() statement in JavaScript. The specification simply does not allow it:

// Security nightmare: There is no way to specify an SRI hash here!
const module = await import('https://cdn.example.com/heavy-module.js');

If you are using dynamic imports to load code from a CDN, or even from your own separate asset domain, those chunks are completely vulnerable to tampering. A strict Content Security Policy (CSP) can limit where scripts can load from, but CSP cannot verify the contents of the file.

For years, the only solution was to intercept the fetch request, download the text, manually hash it using the Web Crypto API, and then evaluate the code. This was slow, complex, and destroyed the browser’s ability to optimize JavaScript parsing.

The Solution: Import Maps

Import Maps were introduced to solve the problem of bare module specifiers in the browser (e.g., allowing you to write import React from 'react' instead of import React from 'https://cdn.com/react.js').

However, a lesser-known feature of Import Maps is that they finally provide a native, platform-level mechanism for applying Subresource Integrity to dynamic imports.

Here is how you secure a dynamic import in 2026:

Step 1: Define the Integrity in the Import Map

In your HTML <head>, you define your Import Map. Crucially, you use the integrity field within the map to associate a specific URL with its cryptographic hash.

<script type="importmap">
{
  "imports": {
    "search-ui": "https://cdn.example.com/search-ui-v2.js"
  },
  "integrity": {
    "https://cdn.example.com/search-ui-v2.js": "sha256-XYZ..."
  }
}
</script>

Step 2: Use the Bare Specifier

Now, in your application code, you execute the dynamic import using the bare specifier defined in the map:

document.querySelector('#search-button').addEventListener('click', async () => {
  try {
    // The browser will automatically intercept this, fetch the URL from the map,
    // and strictly enforce the SRI hash defined in the map's integrity block!
    const { initializeSearch } = await import('search-ui');
    initializeSearch();
  } catch (error) {
    console.error("Failed to load or verify the search module", error);
  }
});

When the browser encounters the import('search-ui'), it looks up the URL in the Import Map. It also checks the integrity dictionary. If a hash exists for that URL, the browser will transparently apply Subresource Integrity to the dynamic fetch. If the hash fails, the Promise rejects.

Automating the Integrity Map

Manually updating hashes in an HTML file every time a vendor script updates is a recipe for broken deployments. You must automate this in your build pipeline.

At Web Specification Studio, we generate our Import Map dynamically during our build step. For example, when integrating Pagefind (a fantastic, heavily code-split static search library), we cannot know the hashes of its dynamic chunks ahead of time.

Our build script does the following:

  1. Runs the Pagefind indexing step, which outputs the final .js chunks into our public directory.
  2. Iterates over those chunks, reading the file streams and generating SHA-256 hashes using Node’s crypto module.
  3. Injects those hashes directly into an Import Map template which is then served in our <head>.
// A simplified Node.js build script
import crypto from 'crypto';
import fs from 'fs';

const fileBuffer = fs.readFileSync('./public/search-ui.js');
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
const base64Hash = hashSum.digest('base64');

const integrityString = `sha256-${base64Hash}`;
// Inject this string into your HTML generation step

CSP Considerations

If you are using a strict Content Security Policy (which you should be), you must ensure that your Import Map itself is allowed to execute.

Because the Import Map is an inline <script> tag, a strict CSP will block it unless you provide a nonce or a hash of the map itself in your script-src directive.

Content-Security-Policy: script-src 'self' 'sha256-HashOfYourImportMapHere...';

Conclusion

Code-splitting and dynamic imports are essential for web performance. But performance cannot come at the expense of supply chain security.

By combining dynamic imports with the integrity feature of JavaScript Import Maps, we finally have a native, elegant way to ensure that every byte of JavaScript executed in our users’ browsers is cryptographically verified, regardless of when or how it was loaded.

Related posts