JavaScript Integrity Manifests with Import Maps
Guy Bedford, August 5th 2024
With the new "integrity"
field for import maps, recently released in Chrome 127 thanks to work from Shopify engineering, we now have a new security primitive for JS modules on the web with the ability to define a module integrity manifest.
Before this, there were only two ways to define Subresource Integrity for JavaScript code on the web:
- With the integrity directly on the script tag:
<script type="module" integrity="sha386-..." src="...">
. - With the integrity directly on a preload tag:
<link rel="modulepreload" integrity="sha386-..." href="...">
.
These requirements for integrity have limitations in that:
- Integrity does not apply for dependency imports, since there is no
"integrity"
attribute planned for something likeimport 'dep' with { "integrity": "..." }
, due to the cascading effect of inline integrity. - Further, there is also no way to apply integrity for lazy dependency imports using dynamic
import()
s. All JS source with integrity must therefore be eagerly fetched and loaded by the network with a direct script or preload tag that can provide the integrity. - Since a given JS module may be imported in multiple places, every single call site is responsible for ensuring the integrity check, making it hard to treat this as a universal security guarantee.
The above often make it inhibitatively difficult to ship ES modules on the web with full integrity for JS resources, or to even consider a Content-Security-Policy: require-sri-for script;
integrity policy when shipping ES modules.
The Import Map "integrity"
field
The import map "integrity" field in import maps allows populating a mapping of URLs to their integrity attributes.
These integrity values are then applied for all JS imports matching the defined URLs, applying both to modules loaded statically on page initialization along with their dependencies, and those lazily loaded after the initial page load.
Having all the module integrity in this new single manifest makes it much easier to verify and update integrity across an application, as well as to support require-sri-for
.
Example
To take advantage of this new field when using JSPM, the JSPM Generator and CLI now support a new integrity
option and flag.
For example, consider the following HTML application app.html
:
<!doctype html>
<script type="importmap">
{
"imports": {
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3.1.4/core/lit-core.min.js"
}
}
</script>
<script type="module" src="./app.js"></script>
<body>
<simple-greeting name="World"></simple-greeting>
</body>
where app.js
contains:
import {html, css, LitElement} from 'lit';
class SimpleGreeting extends LitElement {
static styles = css`p { color: blue }`;
static properties = {
name: {type: String}
};
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
customElements.define('simple-greeting', SimpleGreeting);
We can use the JSPM CLI to update this HTML page to relink the modules outputting the new import map with integrity back into the same HTML page:
jspm link app.html --integrity -o app.html
This will update app.html
to now contain:
<!doctype html>
<script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js" crossorigin="anonymous" integrity="sha384-ie1x72Xck445i0j4SlNJ5W5iGeL3Dpa0zD48MZopgWsjNB/lt60SuG1iduZGNnJn"></script>
<script type="importmap">
{
"imports": {
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3.1.4/core/lit-core.min.js"
},
"integrity": {
"./app.js": "sha384-JBeRlySsOPUakm9Jdnn7Kcmbf/FFAGhbcEcwJkBXYCdtAtG1oVv5/PVycS1nsNKC",
"https://cdn.jsdelivr.net/gh/lit/dist@3.1.4/core/lit-core.min.js": "sha384-1XCsIc9Rfy/YoXO1AeA7koK9Donixq1VQYObT7umyw25v2v8dBBumjdE8cgOg4aW"
}
}
</script>
<script type="module" src="./app.js"></script>
<body>
<simple-greeting name="World"></simple-greeting>
</body>
With the above "integrity"
configuration, we don't need to consider any further integrity attributes at all for a top-level <script type="module" src="">
site, instead the integrity will always be verified for all of the modules imported.
If the network returns a different source for a given JS file, then a network error is thrown, instead of there being a potential vulnerability.
Try it out on the online generator here turning on the
Integrity
toggle at the top of the page to see an example of the generated import map integrity field.
ES Module Shims Support
The ES Module Shims modules polyfills project now fully includes a polyfill for import map "integrity"
.
Whenever the polyfill is engaging (either in shim mode, or when statically unsupported modules features are used), "integrity"
metadata will be passed to the underlying fetch
request used by the polyfill, to ensure that even when the polyfill is engaging integrity is still supported.
While not a comprehensive security model (unless using shim mode), the ability to support "integrity"
when the polyfill is engaging still expands the coverage of the integrity checks for users of an application, still raising the overall level of security.
A Future with Integrity
Integrity should really be the default for all JS applications, especially those relying on third-party CDNs!
This can prevent the fallout of attacks like the recent polyfill.io supply chain attack, where users relying on the polyfill.io
CDN to serve JS code found that code being tampered with to create targeted redirects across all sites embedding these JS polyfills.
To get there, integrity should become a first-class part of our JS deployment workflows, which this new "integrity"
field can help enable.