Supply Chain Security for CDN: Protecting Third-Party Script Dependencies
Every modern website loads JavaScript from third-party CDNs. Analytics libraries, UI frameworks, polyfills, font loaders — these dependencies form a supply chain that most teams never audit after initial integration. When a CDN-hosted library is compromised, every site loading it becomes an attack vector. The Polyfill.io incident proved this is not theoretical: a domain change gave attackers control over scripts embedded in more than 100,000 websites. Securing this supply chain requires understanding the attack surface and layering multiple defense mechanisms. For foundational CDN security concepts, see CDN Security Headers: CSP, HSTS, and Beyond.
The CDN Supply Chain Attack Surface
Third-party scripts loaded from public CDNs like cdnjs, jsDelivr, and unpkg introduce trust dependencies that most developers do not fully map. When you add a script tag pointing to a CDN-hosted library, you trust the CDN operator, the package maintainer, their npm credentials, their CI/CD pipeline, and every transitive dependency in the package tree. A compromise anywhere in this chain delivers malicious code directly into your users' browsers with the full privileges of your origin.
The attack surface has three primary vectors. Account takeover targets npm maintainer credentials through phishing, credential stuffing, or token leakage in CI logs. CDN compromise involves directly tampering with files hosted on a CDN, either through infrastructure access or by exploiting the CDN's build pipeline. Domain takeover occurs when an attacker gains control of a CDN domain through expiration, registrar compromise, or acquisition — as happened with Polyfill.io.
Subresource Integrity: Cryptographic Verification
Subresource Integrity (SRI) is the single most effective defense against CDN supply chain attacks. SRI adds a cryptographic hash to script and link tags. The browser computes the hash of the downloaded file and refuses to execute it if the hash does not match. This guarantees that the exact file you reviewed and tested is the file your users receive, regardless of what happens on the CDN between your deployment and their request.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
<!-- SRI-protected script loading -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"
integrity="sha384-OYlGGIKFt3Xn4sthrV3GpMoVWxBKRvz3PZ2hu5GHxR0oLHBBgaGNfHqBIplDigL"
crossorigin="anonymous"></script>
<!-- SRI for stylesheets -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/normalize.min.css"
integrity="sha384-n8MVjX3btmFCyo+kW3iB40MUEWqFLmjV9LeRzGCqVYMa5GWDSMXht07sFBEVPJC"
crossorigin="anonymous">
The crossorigin="anonymous" attribute is required for SRI to work with cross-origin resources. Without it, the browser cannot access the response body to compute the hash. The integrity attribute accepts multiple space-separated hashes, allowing you to list hashes for different algorithms or different acceptable versions of a file.
Generating SRI Hashes
Generate SRI hashes from the command line or integrate them into your build pipeline:
# Generate sha384 hash for a local file
openssl dgst -sha384 -binary lodash.min.js | openssl base64 -A
# Output: OYlGGIKFt3Xn4sthrV3GpMoVWxBKRvz3PZ2hu5GHxR0oLHBBgaGNfHqBIplDigL
# Generate hash for a remote URL
curl -s https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js \
| openssl dgst -sha384 -binary | openssl base64 -A
# Using the srihash.org API or shasum
shasum -b -a 384 lodash.min.js | awk '{ print $1 }' | xxd -r -p | base64
Automate SRI hash generation in your CI/CD pipeline. Every time a dependency is updated, regenerate and commit the new hash. This creates an auditable trail of exactly which file content was approved for production.
Content Security Policy for Script Control
CSP restricts which origins can serve executable scripts to your page. While SRI verifies file integrity, CSP controls the allowed sources. Together they form a defense-in-depth strategy: CSP blocks scripts from unauthorized domains, and SRI blocks tampered scripts from authorized domains. For a complete CSP implementation guide, see CDN Security Headers.
# Strict CSP with nonce-based script loading
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{random}' 'strict-dynamic'
https://cdnjs.cloudflare.com
https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline'
https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
img-src 'self' data: https:;
frame-ancestors 'none';
base-uri 'self';
require-sri-for script style;
The require-sri-for directive forces SRI verification on all script and style loads. If a tag lacks an integrity attribute, the browser blocks it. This prevents developers from accidentally bypassing SRI by forgetting the hash on a new dependency.
Import Maps: Centralized Dependency Resolution
Import maps provide a browser-native mechanism for mapping JavaScript module specifiers to specific URLs. Instead of scattering CDN URLs across hundreds of script tags, you define a single JSON mapping that the browser uses to resolve bare import specifiers. This centralizes version management and makes CDN provider switches a single-file change.
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js",
"react": "https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js",
"chart.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"
},
"scopes": {
"/admin/": {
"react": "https://cdnjs.cloudflare.com/ajax/libs/react/18.3.0/umd/react.production.min.js"
}
}
}
</script>
<!-- Now modules can use bare specifiers -->
<script type="module">
import _ from 'lodash';
import React from 'react';
</script>
The scopes feature lets different parts of your application use different versions of the same package. The admin section in this example uses React 18.3 while the rest of the site uses 18.2. This is useful during gradual migrations or when different features have incompatible dependency requirements.
Version Pinning and Lock Files
Public CDNs like jsDelivr and unpkg support version ranges in URLs. A URL like /npm/lodash@^4/lodash.min.js resolves to the latest compatible version, which changes without your knowledge or consent. This is a supply chain risk: a compromised package version automatically propagates to your production site.
<!-- DANGEROUS: version range auto-updates -->
<script src="https://cdn.jsdelivr.net/npm/lodash@^4/lodash.min.js"></script>
<!-- SAFE: exact version pinned -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"
integrity="sha384-OYlGGIKFt3Xn4sthrV3GpMoVWxBKRvz3PZ2hu5GHxR0oLHBBgaGNfHqBIplDigL"
crossorigin="anonymous"></script>
<!-- SAFEST: exact version + exact file hash -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"
integrity="sha384-OYlGGIKFt3Xn4sthrV3GpMoVWxBKRvz3PZ2hu5GHxR0oLHBBgaGNfHqBIplDigL
sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ=="
crossorigin="anonymous"></script>
Always pin exact versions in CDN URLs and combine them with SRI hashes. Treat CDN URL updates the same way you treat package.json updates: review the changelog, run tests, and commit the change with a clear description of what changed and why.
Self-Hosting as a Defense Strategy
Self-hosting third-party scripts eliminates the CDN as a trust dependency entirely. You download the exact version you need, verify its hash, bundle it with your application, and serve it from your own origin infrastructure. The trade-off is losing CDN cache sharing (a user who loaded lodash from cdnjs on another site will not have it cached for yours) and accepting the operational burden of monitoring and updating dependencies yourself.
# Self-hosting workflow
# 1. Download the exact version
curl -o vendor/lodash-4.17.21.min.js \
https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
# 2. Verify the hash
echo "expected_hash vendor/lodash-4.17.21.min.js" | sha384sum -c
# 3. Commit to version control with the hash
git add vendor/lodash-4.17.21.min.js
git commit -m "vendor: add lodash 4.17.21 (sha384: expected_hash)"
# 4. Serve from your own CDN
# script src="/vendor/lodash-4.17.21.min.js"
For high-security applications — banking, healthcare, government — self-hosting is the recommended approach. The performance penalty is minimal when your own CDN delivers the files, and you eliminate an entire class of supply chain attacks.
Build-Time Verification Pipeline
Integrate supply chain checks into your CI/CD pipeline to catch compromised dependencies before they reach production. A verification pipeline should check three things: that pinned versions match lock file expectations, that SRI hashes in HTML match the actual files, and that no new domains have been added to script sources without review.
#!/bin/bash
# ci/verify-supply-chain.sh
# Check all script tags have SRI
echo "Checking SRI coverage..."
grep -rn '<script.*src=' dist/ | grep -v 'integrity=' | grep -v 'type="application/ld+json"'
if [ $? -eq 0 ]; then
echo "ERROR: Scripts without SRI found"
exit 1
fi
# Verify SRI hashes match downloaded files
echo "Verifying SRI hashes..."
for url in $(grep -ohP 'src="(https://[^"]+)"' dist/index.html | grep -oP 'https://[^"]+'); do
expected=$(grep -oP "integrity=\"sha384-[^\"]+\"" dist/index.html | head -1 | grep -oP 'sha384-[^"]+')
actual=$(curl -s "$url" | openssl dgst -sha384 -binary | openssl base64 -A)
if [ "sha384-$actual" != "$expected" ]; then
echo "HASH MISMATCH: $url"
exit 1
fi
done
# Check CSP allows only known CDN domains
echo "Checking CSP domain allowlist..."
allowed="cdnjs.cloudflare.com cdn.jsdelivr.net fonts.googleapis.com fonts.gstatic.com"
# ... validate against CSP header
Monitoring and Incident Response
Even with SRI and CSP in place, you need monitoring to detect supply chain attacks that bypass your defenses or target vectors you have not covered. CSP violation reports are the first line of detection. Configure the report-uri or report-to directive to send CSP violations to a monitoring endpoint, and alert on anomalies. For comprehensive monitoring strategies, see CDN Monitoring and Observability.
Monitor your CDN dependencies for known vulnerabilities using services like Snyk, Socket.dev, or GitHub's Dependabot. These tools analyze your dependency tree for packages with known security issues and alert you when a package you use is compromised. For npm-sourced CDN dependencies, subscribe to security advisories for every package in your dependency tree.
Build an incident response playbook for supply chain compromises. When a CDN-hosted library is compromised, your response should include: immediately replacing the CDN reference with a self-hosted copy, rotating any credentials or tokens that could have been exfiltrated, scanning your CSP violation logs for evidence of data exfiltration, and notifying users who may have been affected. The faster you can detect and respond, the smaller the blast radius. See CDN Incident Response Playbooks for detailed procedures.