A “Missing Subresource Integrity” finding means a page loads JavaScript or CSS from another origin, typically a CDN, without an integrity attribute, so the browser runs whatever that server returns. Fix it by adding integrity="sha384-..." and crossorigin="anonymous" to each third-party script and stylesheet tag, pinning exact versions, or self-hosting the files.
What the scanner is actually detecting
Subresource integrity (SRI) is a browser feature. You put a base64-encoded SHA hash of a file in the integrity attribute of the tag that loads it, and the browser refuses to use the file if the downloaded bytes produce a different hash. The scanners below crawl your pages and flag script and link tags that point at another host but carry no integrity value. The checks are passive: no attack payload is sent.
| Scanner | Finding title | ID | Vendor rating |
|---|---|---|---|
| Tenable Web App Scanning | Missing Subresource Integrity | Plugin 98647 | Info |
| Tenable Web App Scanning | Invalid Subresource Integrity | Plugin 98649 | Medium |
| ZAP | Sub Resource Integrity Attribute Missing | Passive rule 90003 | Medium |
| Acunetix | Subresource Integrity (SRI) Not Implemented | See your scan report | Informational |
| Burp Scanner | Cross-domain script include | Type index 0x00500500 | Information |
Plugin 98649 is different and more urgent: an integrity attribute exists, but its hash no longer matches the file the scanner downloaded. ZAP’s rule covers script and link tags served by external servers. Burp’s issue is broader: it reports any script included from another domain and lists SRI or self-hosting as remediations.
Real-world risk
Without SRI, your page trusts the CDN completely. Anyone who can change the file on that server (through a compromised CDN account, a domain that changes hands, or a malicious package release) runs code in your users’ browsers inside your application’s security context, with access to form fields and the ability to send requests as the logged-in user.
This has happened. In 2024 the cdn.polyfill.io service came under new ownership and began serving malicious JavaScript to sites that embedded it, as documented by Sansec. The case also shows the limit of SRI: polyfill.io generated its code dynamically from request headers, so no fixed hash was possible, and the remedy was removing or self-hosting it.
On its own, the finding is a missing hardening control, not an exploitable bug. Weight it higher on login, checkout and admin pages. Investigate 98649 (hash mismatch) the same day.
How to confirm it on the host
Start with the URL in the finding. This lists script and link tags that load from an absolute or protocol-relative URL and have no integrity attribute:
curl -s https://www.example.com/
| grep -Eio '<(script|link)[^>]*>'
| grep -Ei "(src|href)=["']?(https?:)?//"
| grep -vi 'integrity='
Drop lines that point at your own hostname, and ignore link tags whose rel is not stylesheet, preload or modulepreload, because SRI only applies to those. grep misses tags split across lines and tags that other scripts insert at runtime, so check the rendered page too. Paste this into the DevTools console on the flagged page:
console.table([...document.querySelectorAll(
'script[src], link[rel~="stylesheet"], link[rel="preload"], link[rel="modulepreload"]'
)].map(e => ({ url: e.src || e.href, integrity: e.integrity }))
.filter(r => new URL(r.url, location.href).origin !== location.origin && !r.integrity));
Each row is a third-party resource with no integrity value. To check whether an existing value is still correct (the 98649 case), hash the live file and compare the result with the text after sha384- in the tag:
curl -s https://cdn.example.net/lib/2.4.1/lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A; echo
The CDN must also allow CORS, or SRI cannot work for a cross-origin file. Look for Access-Control-Allow-Origin: * or your own origin:
curl -s -o /dev/null -D - -H "Origin: https://www.example.com"
https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css
| grep -i '^access-control-allow-origin'
How to fix it
Step 1: pin an exact version
A hash only works against a file that never changes. URLs such as .../npm/bootstrap@5/... or @latest resolve to a new file on each release, which would break an SRI-protected page. Use a full version, for example https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css.
Step 2: generate the hash
MDN documents this OpenSSL pipeline. Run it against the exact file you will load, then prefix the output with sha384-:
curl -s https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css
| openssl dgst -sha384 -binary | openssl base64 -A
# for a file already on disk
cat lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A
On Windows without OpenSSL, PowerShell produces the same value from a local copy. Do not use Get-FileHash output directly: it is hexadecimal, and SRI needs base64.
$bytes = [System.IO.File]::ReadAllBytes('C:webvendorlib.min.js')
$sha = [System.Security.Cryptography.SHA384]::Create()
'sha384-' + [Convert]::ToBase64String($sha.ComputeHash($bytes))
Step 3: add integrity and crossorigin to the tag
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
integrity="sha384-HASH_FROM_STEP_2"
crossorigin="anonymous">
<script src="https://cdn.example.net/lib/2.4.1/lib.min.js"
integrity="sha384-HASH_FROM_STEP_2"
crossorigin="anonymous"></script>
The crossorigin="anonymous" attribute is required. Without it the browser fetches the file in no-cors mode, the SRI check cannot pass, and the resource is blocked. SHA-256, SHA-384 and SHA-512 are all accepted; if you list several hashes, the browser uses the strongest algorithm present.
Step 4 (bundled apps): let the build add the hashes
If webpack produces your bundles and they are served from a CDN hostname, the webpack-subresource-integrity plugin (current releases target webpack 5) adds integrity values to tags that html-webpack-plugin injects and to dynamically loaded chunks:
npm install webpack-subresource-integrity --save-dev
// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
module.exports = {
output: {
crossOriginLoading: 'anonymous',
},
plugins: [
new SubresourceIntegrityPlugin({ hashFuncNames: ['sha384'] }),
],
};
SHA-384 is the plugin’s default, and in its default auto mode it runs only for production builds. Use [contenthash] file names so browsers never pair a new hash with a cached old file, and keep the plugin off under webpack-dev-server, where it interferes with hot reloading. Tags you hard-code in the HTML template are not webpack assets, so hash those by hand as in step 2.
Alternative: self-host the file
Downloading the pinned file (or installing it from npm and bundling it) and serving it from your own origin removes the third-party dependency, and the scanner no longer sees a cross-origin include. Put the self-hosted copy on your normal patch cycle, because it no longer changes unless you change it.
Resources that cannot take a static hash
Tag managers, many analytics and chat widgets, and services that generate CSS or JavaScript per browser change content at a fixed URL. OWASP’s third-party JavaScript cheat sheet warns that SRI can leave you with secure but non-working code once the vendor updates a script. For these, record a documented exception, restrict where scripts may load from with a Content-Security-Policy script-src allowlist, and review the vendor periodically. MDN also documents a newer Integrity-Policy header that blocks scripts or styles without integrity metadata; it has limited browser availability, so start with Integrity-Policy-Report-Only.
How to verify the fix and rescan
- Deploy, then reload the page with DevTools open and the cache disabled. The console should show no integrity errors, and every third-party script and stylesheet should load in the Network tab.
- Run the console snippet and the grep check again on each flagged URL, including login, error and checkout pages, which often have their own tags. Empty output means every third-party file carries a hash.
- Rescan. Tenable WAS needs a new scan of the application; ZAP’s rule is passive, so re-spidering the pages is enough; Acunetix and Burp need a rescan of the affected paths.
SRI findings come from crawling rendered pages, so a source code or dependency scan will not close them. See how SAST, DAST and SCA results differ for why the same library can appear in several reports.
What can break and how to roll back
- Hash mismatch: the browser treats the file as a network error, so the script does not run or the stylesheet does not apply. Typical causes are a floating version URL, a vendor updating a file in place, or a proxy that rewrites content. The webpack plugin documentation recommends
Cache-Control: no-transformwhere a proxy might modify assets. - Missing crossorigin attribute or CORS header: the file is blocked even though the hash is correct.
- Stale caches: a new hash paired with an old cached file fails the check, which is why unique file names per build matter.
Rollback is a template change: remove the integrity attribute from the affected tag, or redeploy the previous build. Keep the version pin even if you roll back. If Tenable later raises 98649 on a tag you hashed, follow its solution text: check whether the third-party file was modified, update the hash only if the change is legitimate, and stop using the resource if it is not.
Common false positive reasons
Most of these are accepted-risk cases rather than scanner errors.
- Link tags SRI does not cover: rel values such as preconnect, dns-prefetch, icon or canonical cannot use integrity. If a scanner flags one, document it.
- Your own asset hostname: a static or CDN hostname you control still counts as a different origin. The finding is technically accurate; the build plugin closes it cleanly.
- Script-inserted tags: resources added at runtime by a tag manager or widget cannot carry an attribute from your HTML. Track them as exceptions with compensating controls.
- Per-browser content: the Google Fonts stylesheet API, for example, returns CSS tailored to the requesting browser, so one hash cannot match every visitor. Self-host the fonts or accept the finding.
- Stale results: a page cache or CDN served old HTML to the scanner after you deployed.
FAQ
Is missing SRI a vulnerability by itself?
No. It is a missing defense against a compromised third-party host. Tenable and Acunetix rate it informational, while ZAP rates it medium.
Which hash algorithm should I use?
SHA-384 is the common choice: it is the webpack plugin default and the value ZAP calculates. The W3C specification requires browsers to support SHA-256, SHA-384 and SHA-512.
Do same-origin files need integrity attributes?
These checks target files from other hosts, so same-origin files do not trigger them. Hashing your own bundles is harmless, and the webpack plugin does it automatically.
Does a Content-Security-Policy replace SRI?
No. A CSP allowlist controls which hosts may serve scripts; SRI controls what those files contain. A tampered file on an allowed CDN passes CSP but fails SRI, so use both.
Tracking this finding across many hosts
A shared header or footer template can put the same unhashed CDN tag on every site you run. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners and merges duplicates within each scanner but not across them, so a page flagged by two tools stays as two records. For Acunetix and Burp findings it can retest an individual finding after the template change, so closure rests on a fresh check rather than a ticket status.