The CDN Supply Chain Risk
Many websites load JavaScript and CSS from Content Delivery Networks (CDNs). CDNs reduce latency and shift bandwidth costs, but they introduce a trust problem: you are executing code from a server you don't control.
If a CDN is compromised, every site using it becomes vulnerable. This is not theoretical:
- Magecart attacks: attackers inject credit card skimming code into CDN-hosted JavaScript used by e-commerce sites. British Airways (2018, ~500K customers), Ticketmaster (2018), and many others were breached this way.
- event-stream incident (2018): a malicious contributor added cryptocurrency theft code to a popular npm package. Downstream CDNs served the malicious version before detection.
Subresource Integrity (SRI) is the browser-native defense against these attacks.
How SRI Works
SRI adds a cryptographic hash to your <script> and <link> tags. The browser downloads the resource, computes its hash, and only executes/applies it if the hash matches.
<!-- Without SRI: executes whatever cdn.example.com returns -->
<script src="https://cdn.example.com/jquery-3.7.1.min.js"></script>
<!-- With SRI: only executes if hash matches -->
<script
src="https://cdn.example.com/jquery-3.7.1.min.js"
integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs"
crossorigin="anonymous"
></script>
If the CDN serves a tampered file, the hash won't match and the script is blocked. The browser logs a console error and continues without executing the malicious code.
The crossorigin Attribute
The crossorigin="anonymous" attribute is required for SRI to work on cross-origin resources. It tells the browser to make a CORS request without credentials (no cookies). The CDN must respond with Access-Control-Allow-Origin: * or the resource load will fail.
If you omit crossorigin, the browser cannot access the response body to verify the hash — SRI silently fails to work.
Hash Algorithm Selection
| Algorithm | Status | Recommendation |
|---|---|---|
| SHA-256 | Supported | Minimum acceptable |
| SHA-384 | Supported | Recommended default |
| SHA-512 | Supported | Maximum security |
| SHA-1, MD5 | Not supported | Not accepted by browsers |
Use SHA-384. It provides strong security without the marginal overhead of SHA-512.
You can include multiple hashes for the same resource — useful during algorithm migration or when supporting multiple CDN copies:
<script
src="..."
integrity="sha384-<hash1> sha512-<hash2>"
crossorigin="anonymous"
></script>
The browser accepts the resource if any of the listed hashes match.
Generating SRI Hashes
With openssl
# Download the file first
curl -sL https://cdn.example.com/library.min.js -o library.min.js
# Generate SHA-384 hash in the SRI format
openssl dgst -sha384 -binary library.min.js | openssl base64 -A
# Prepend 'sha384-' to the output
# One-liner
echo "sha384-$(openssl dgst -sha384 -binary library.min.js | openssl base64 -A)"
With shasum (built into macOS/Linux)
cat library.min.js | openssl dgst -sha384 -binary | openssl enc -base64 -A
srihash.org
For quick lookups, paste a CDN URL into srihash.org and it generates the correct integrity attribute for you.
CI/CD Integration
Automate hash generation as part of your build pipeline. If your project pulls CDN versions from a config file, verify hashes in CI:
# scripts/verify_sri_hashes.py
import hashlib
import base64
import urllib.request
ASSETS = [
{
'url': 'https://cdn.example.com/library-3.7.1.min.js',
'expected': 'sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs',
},
]
for asset in ASSETS:
with urllib.request.urlopen(asset['url']) as f:
content = f.read()
digest = hashlib.sha384(content).digest()
computed = 'sha384-' + base64.b64encode(digest).decode()
if computed != asset['expected']:
print(f'MISMATCH: {asset["url"]}')
print(f' Expected: {asset["expected"]}')
print(f' Computed: {computed}')
exit(1)
else:
print(f'OK: {asset["url"]}')
Fallback Strategies
If the CDN resource fails the SRI check — or if the CDN is unavailable — you need a fallback to your own hosted copy:
<script
src="https://cdn.example.com/library.min.js"
integrity="sha384-<hash>"
crossorigin="anonymous"
onerror="loadFallback()"
></script>
<script>
function loadFallback() {
var s = document.createElement('script');
s.src = '/static/vendor/library.min.js';
document.head.appendChild(s);
}
</script>
The onerror fires both on network failure and SRI mismatch. The fallback loads your self-hosted copy, which you should keep in sync with the CDN version.
Note: If you're using CSP with SRI, ensure your script-src allows both the CDN host and 'self' so the fallback is permitted.
When Not to Use Fallback
If SRI fails because the CDN served a tampered file, a fallback to a local copy is the right response. But if you're serving the same file from your own origin anyway, skip the CDN entirely — the performance benefit disappears when the CDN and your origin are in the same region, and you eliminate the supply chain risk.