Shipping support for module script integrity in Chrome & Safari

Shipping support for module script integrity in Chrome & Safari

 

Shopify’s checkout is a managed service that provides high-performance, PCIv4 DSS compliant checkout. A key technical requirement to deliver this is the ability to control which scripts are included and run during checkout to ensure their contents aren’t tampered with. Subresource Integrity (SRI) is a web platform capability that, on the surface, is the right tool for the job: it validates resource downloads via integrity hashes, blocking execution if there is a mismatch; it ensures that resources remain consistent from generation in the build process to delivery to users.

However, SRI had a critical capability gap, because it only applied to top-level scripts, styles, and preloads, and did not support imported JavaScript modules. This limitation prevented us from using the web platform’s import() function and required shimming functionality for dynamic script imports, creating maintenance challenges and adding runtime overhead.

To address this, we revived Guy Bedford's proposal to integrate integrity metadata into import maps and landed support for subresource integrity in imported module scripts in Chromium (Chrome, Edge) and Webkit (Safari) based browsers. 

Implementation journey

The proposal essentially uses import maps in order to map module URLs to their integrity hash, which seemed like an elegant solution that doesn’t reintroduce the cache invalidation cascades that import maps help us avoid.

 

 

That enables us to use dynamic imports without any change (e.g. import("./module/shapes/square.js")), and be assured that the module's integrity is verified via the provided hash before execution.

Initial assessment of the importmap specification and its Chromium implementation also looked promising. The spec was in a really good shape and the code followed it very closely. There were already two existing sections in import maps, which gave us confidence that we can land “integrity” as a third one.

A scrappy working Chromium implementation, spec draft update, and rudimentary test suite came together on a flight — with no in-seat power! The relevant spec and Chromium code owners provided feedback quickly, indicating the solution should cover static imports, top-level module scripts and `modulepreload`. Overall, iteration took 32 commits for the Chromium implementation and 21 commits for the HTML PR. At the end, we had a spec PR, Chromium CL and tests all ready to go.

Next, we (belatedly) sent an intent to prototype, and asked for WebKit, Mozilla and TAG positions. The feedback came back positive and encouraged by WebKit’s position, we decided to also implement the feature there. Implementing in another engine required extra work as the underlying script-loading mechanisms are quite different from Chromium, especially for static imports. With two implementations under our belt and a successful WHATWG review, the HTML spec PR was ready to land, and we sent out an intent to ship in Chromium.

Shipping in Chrome and Safari

Chromium’s intent-to-ship was approved, the HTML PR landed, and the feature shipped with Chromium 127 🎉 reaching Chrome and Edge’s stable channels last week. The feature also landed in Safari 18’s Beta release, and will presumably reach Safari’s stable version in the next couple of months. So if you wanted to use ES modules and dynamic imports but the lack of SRI was holding you back, you can start using them now!

Learnings

Having a pre-existing proposal significantly accelerated the process. Putting forward a proposal without a clear plan for how it’d get implemented in browser engines can often be a frustrating exercise. But in this particular case it really made all the difference. Thanks Guy!

Beyond that, shipping new capabilities on the web platform doesn’t have to be a long ordeal. Small, well-defined and non-controversial additions can be added to the platform in a matter of weeks. Overall, shipping ES module integrity took slightly over two weeks of full-time work, and about seven chronological weeks from start to finish.

Finally, you don’t have to be a browser vendor in order to be a browser implementer. Contributing the feature’s implementation to multiple rendering engines felt great and doing that as part of my job at Shopify was doubly great: we unlocked a missing primitive for Shopify’s platform and made both commerce and the web platform better for everyone. Sustainable open source for the win!

 

Thanks to Ilya Grigorik and Surma for their reviews and contributions to this post.

 

 

Back to blog