Third-Party Risk Assessment
Permalink to "Third-Party Risk Assessment"This workflow is part of Supply Chain Auditing & Dependency Verification. Without a structured risk assessment, engineering teams have no consistent basis for deciding which external origins require cryptographic enforcement, which packages need hash-pinning, or when a dependency vulnerability crosses the threshold that demands immediate remediation. The result is an ad-hoc security posture that looks thorough on paper but collapses under a real supply-chain incident: a tampered CDN asset executes silently, a compromised npm package ships to production because no gate blocked it, and the post-mortem reveals no documented owner for the affected dependency.
Prerequisites
Permalink to "Prerequisites"Conceptual Foundation: Trust Boundaries in the Browser Fetch Lifecycle
Permalink to "Conceptual Foundation: Trust Boundaries in the Browser Fetch Lifecycle"A trust boundary is the point at which your application accepts data or code from a source it cannot fully control. In the browser, that boundary is crossed every time a <script> or <link> tag fetches a resource from an external origin. The W3C Subresource Integrity specification (Level 1, published August 2016, living standard maintained at w3.org/TR/SRI/) addresses this by mandating that the browser compute the cryptographic digest of the fetched bytes and compare it against the hash the developer embedded in the integrity attribute — before any bytes enter the JavaScript engine. If the digests differ, the browser refuses to execute the resource and fires a CSP violation event.
This mechanism shifts trust from “I trust this URL to always return safe bytes” to “I trust this specific byte sequence”. The distinction matters enormously in supply-chain threat modelling: an attacker who can modify a file on a CDN can serve malicious JavaScript from a perfectly legitimate URL. SRI breaks that attack vector.
The same principle applies to the npm registry. A dependency that was safe at v2.3.1 may be compromised at v2.3.2 — or a malicious actor may publish a typosquatted package that differs by one character in its name. Lockfile hash-pinning extends the SRI concept to the package install phase: package-lock.json stores the integrity field (a SHA-512 sha512- SRI hash) for every resolved package, so npm ci verifies the downloaded tarball before extracting it.
Step 1 — Build the Dependency Inventory
Permalink to "Step 1 — Build the Dependency Inventory"Enumerate every external origin and package your application consumes. Split the inventory into three layers:
Layer 1 — CDN-hosted assets loaded by the browser. These are <script src="..."> and <link rel="stylesheet" href="..."> tags pointing to origins outside your control. Common examples: Google Fonts, jsDelivr, unpkg, Cloudflare CDN.
Layer 2 — npm/pnpm/Yarn packages installed at build time. Both direct dependencies listed in package.json and the full transitive graph resolved in the lockfile. Use Lockfile Mapping & Analysis to automate this; manual counting above ~50 packages is error-prone.
Layer 3 — Runtime API calls and service integrations. Analytics beacons, payment SDKs, authentication providers. These are not covered by SRI (they are fetch()/XHR calls, not subresource loads) but they belong in your risk register for CSP connect-src scoping and network egress policy.
Record each dependency with at minimum: origin URL or package name, version or tag, data classification of assets it can access, and the name of the team or individual responsible for that dependency.
Step 2 — Assign Trust Tiers and Score Risk
Permalink to "Step 2 — Assign Trust Tiers and Score Risk"A trust tier is a named, documented level of assumed reliability for an external origin. Assigning tiers consistently lets you apply proportionate controls — stricter enforcement for high-tier dependencies, lighter monitoring for low-risk ones — rather than applying the same overhead to everything.
| Tier | Criteria | Required Controls |
|---|---|---|
| T1 — Trusted | First-party subdomains, internal CDNs, registries you operate | Lockfile + internal hash manifest; CSP self |
| T2 — Verified | Major CDNs (jsDelivr, cdnjs), packages with >1M weekly downloads, audited maintainers | SRI integrity hash; CSP allowlist by origin |
| T3 — Conditional | Mid-tier packages, single-maintainer libraries, recently published | SRI hash + lockfile pin + automated CVE scan on every build |
| T4 — Untrusted | Unknown origins, packages with <3 months history, unverified provenance | Block from production; review and promote or reject |
Risk scoring within each tier should weight three factors: blast radius (how much of your application can the dependency affect?), update frequency (frequently changing assets have higher drift risk), and maintainer transparency (is the project actively maintained with a public security policy?).
For CDN origins specifically, the trust tier feeds directly into your CDN Trust Mapping & Routing policy, which governs which CDN origins are permitted in CSP and which must fall back to self-hosted copies.
Step 3 — Generate and Embed SRI Hashes
Permalink to "Step 3 — Generate and Embed SRI Hashes"For every T2/T3 CDN asset identified in Step 1, generate a SHA-384 hash of the exact bytes that will be served and embed it in the integrity attribute alongside crossorigin="anonymous".
# compute SHA-384 for a locally downloaded asset
curl -sL https://cdn.example.com/[email protected]/dist/lib.min.js \
| openssl dgst -sha384 -binary \
| openssl base64 -A \
| awk '{print "sha384-"$0}'
Expected output:
sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
Embed it in the HTML:
<script
src="https://cdn.example.com/[email protected]/dist/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
defer
></script>
The crossorigin="anonymous" attribute is not optional. Without it, the browser sends a no-CORS request that returns an opaque response. Opaque responses have no readable body, so the browser cannot compute a hash for comparison, and the integrity check silently fails — the resource loads without verification regardless of the hash you supplied. This is the single most common SRI implementation bug in production deployments.
For build-time automation, use the Node.js crypto module inside your bundler pipeline:
// scripts/generate-sri-manifest.js
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
const assets = [
{ name: 'app', path: resolve('./dist/app.min.js') },
{ name: 'vendor', path: resolve('./dist/vendor.min.js') },
];
const manifest = {};
for (const { name, path } of assets) {
const bytes = readFileSync(path);
const digest = createHash('sha384').update(bytes).digest('base64');
manifest[name] = `sha384-${digest}`;
}
writeFileSync('./dist/sri-manifest.json', JSON.stringify(manifest, null, 2));
console.log('SRI manifest written:', manifest);
This manifest is then consumed by your server-side template engine or Next.js _document to stamp every <script> tag at render time, ensuring build artifacts and HTML always stay in sync.
For a complete walkthrough of integrating hash generation into a Webpack build, see Automating Hash Generation in Webpack 5.
Step 4 — Gate the CI/CD Pipeline
Permalink to "Step 4 — Gate the CI/CD Pipeline"A CI gate that fails on hash mismatch turns your inventory and SRI hashes from aspirational documentation into enforced policy. Without it, a developer who forgets to update a hash after a CDN upgrade can ship an asset that the browser will refuse to load — or worse, a compromised asset that bypasses an outdated hash.
The following GitHub Actions example validates the SRI manifest against the current build output before allowing a deploy:
# .github/workflows/sri-gate.yml
name: SRI Integrity Gate
on: [push, pull_request]
jobs:
build-and-verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies (frozen lockfile)
run: npm ci
- name: Build deterministic bundles
run: npm run build
- name: Generate SRI manifest
run: node scripts/generate-sri-manifest.js
- name: Verify manifest against expected hashes
run: node scripts/verify-sri-manifest.js --fail-on-mismatch
- name: Audit npm dependencies
run: npm audit --audit-level=high
The verify-sri-manifest.js script compares the freshly generated manifest against the hashes committed in expected-sri.json. Any divergence causes a non-zero exit code and blocks the deploy:
// scripts/verify-sri-manifest.js
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import process from 'node:process';
const generated = JSON.parse(readFileSync(resolve('./dist/sri-manifest.json'), 'utf8'));
const expected = JSON.parse(readFileSync(resolve('./expected-sri.json'), 'utf8'));
let failed = false;
for (const [name, hash] of Object.entries(expected)) {
if (generated[name] !== hash) {
console.error(`HASH MISMATCH for "${name}"`);
console.error(` expected: ${hash}`);
console.error(` generated: ${generated[name] ?? '(missing)'}`);
failed = true;
}
}
if (failed) {
console.error('\nRe-run `npm run build && node scripts/generate-sri-manifest.js` and commit the updated expected-sri.json.');
process.exit(1);
}
console.log('All SRI hashes verified.');
The npm ci command in the first step also validates every package tarball against the integrity field in package-lock.json, providing a second hash-verification layer at the package install stage — this is the lockfile-level counterpart to browser-side SRI.
For organisations using Automated SBOM Generation, the SBOM produced during this step captures the verified hash state and becomes the compliance artefact for PCI DSS 6.3.2 (maintaining a software bill of materials) and NIST SSDF PW.4 (reusing well-secured software).
Step 5 — Configure Runtime Monitoring and Incident Response
Permalink to "Step 5 — Configure Runtime Monitoring and Incident Response"Browser-side SRI enforcement generates CSP violation reports whenever the browser refuses a resource because its digest does not match. These reports are the runtime telemetry layer of your third-party risk programme.
Configure your Nginx or Cloudflare Workers origin to emit a CSP header that routes violation reports to your collection endpoint:
# nginx.conf
add_header Content-Security-Policy
"default-src 'self';
script-src 'self' https://cdn.example.com 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC';
style-src 'self' https://fonts.googleapis.com;
report-uri /api/csp-violations;
report-to csp-endpoint;"
always;
For broader browser coverage, pair report-uri (older directive, wide support) with report-to (Reporting API v1). The Configuring Content-Security-Policy with SRI page documents the full header composition for production deployments.
A violation payload looks like this:
{
"csp-report": {
"document-uri": "https://example.com/checkout",
"violated-directive": "script-src",
"blocked-uri": "https://cdn.example.com/[email protected]/dist/lib.min.js",
"source-file": "",
"status-code": 200
}
}
Classify each incoming violation by severity:
- P1 — Immediate response: a CDN-hosted script was blocked (
blocked-uriis a known trusted origin). This may indicate active supply-chain compromise. Trigger your incident response runbook within 15 minutes. - P2 — Investigation within 24 hours: an unknown origin was blocked. Could be a browser extension, a legitimate A/B testing tool you forgot to allowlist, or a shadow IT integration.
- P3 — Backlog: inline script violation (
blocked-uri: inline). Evaluate whether the inline script can be replaced with a nonce or hash in the next sprint.
Configuration Reference
Permalink to "Configuration Reference"| Parameter | Valid Values | Security Implication |
|---|---|---|
integrity algorithm |
sha256-, sha384-, sha512- |
SHA-384 is required minimum for new implementations; SHA-256 is insufficient for high-value assets |
crossorigin |
anonymous, use-credentials |
Must be anonymous for public CDN assets; omitting it disables the integrity check silently |
CSP require-sri-for |
script, style, script style |
Enforces that all matching resources carry a valid integrity attribute — blocks any unattributed load |
| Lockfile mode | npm ci, pnpm --frozen-lockfile, yarn --immutable |
Each refuses to install if the lockfile is inconsistent; never use npm install in CI |
| SBOM format | CycloneDX 1.5, SPDX 2.3 | CycloneDX maps directly to Generating CycloneDX SBOMs for Frontend Assets; prefer for tooling ecosystem compatibility |
| Hash rotation SLA | 24h (P1), 72h (P2), next-sprint (P3) | Define the SLA in your incident response runbook; P1 violations must trigger an emergency deploy |
Integration with Adjacent Tooling
Permalink to "Integration with Adjacent Tooling"The output of this risk assessment workflow feeds directly into the broader supply-chain pipeline:
-
Dependency inventory → lockfile analysis. The package list you catalogue here is the input to Lockfile Mapping & Analysis, which resolves the full transitive graph and surfaces version drift.
-
Lockfile + SRI hashes → SBOM. The verified hashes become provenance data in a CycloneDX or SPDX SBOM, which satisfies PCI DSS 6.3.2 and enables downstream vulnerability correlation in tools like Dependency-Track.
-
SBOM → vulnerability tracking. The SBOM feeds Vulnerability Tracking & Triage, which matches components against CVE databases and prioritises remediation based on your trust tier assignments.
-
CI gate output → CSP policy. The set of approved CDN origins verified by the CI gate becomes the explicit allowlist in your CSP
script-srcdirective, ensuring no CDN origin reaches production without both a hash and a CSP entry.
Troubleshooting
Permalink to "Troubleshooting"Subresource Integrity: The resource 'https://cdn.example.com/...' has an integrity attribute, but the resource requires the request to be CORS enabled.
Root cause: crossorigin attribute is missing or set to a value other than anonymous. Fix: add crossorigin="anonymous" to the tag. If the CDN does not send CORS headers (Access-Control-Allow-Origin: *), the resource cannot be SRI-verified from a cross-origin context — switch to a self-hosted copy.
Failed to find a valid digest in the 'integrity' attribute for resource...
Root cause: the hash in the integrity attribute does not match the bytes the CDN served. Most common causes: (a) the CDN asset was updated after you computed the hash; (b) the CDN is serving a minified version with different whitespace to the version you hashed locally; © the hash was computed against a gzip-compressed response — SRI hashes must be computed against the uncompressed bytes. Fix: re-download the file with curl -sL (no --compressed) and recompute.
npm ci fails with npm ERR! sill integrity sha512-... Integrity check failed
Root cause: the tarball downloaded from the registry does not match the integrity hash in package-lock.json. This can indicate a compromised package on the registry or a corrupted local cache. Fix: clear the npm cache (npm cache clean --force), re-run npm ci. If the failure persists, treat it as a potential supply-chain incident and audit the package on the registry.
CSP require-sri-for blocks a stylesheet you cannot hash (e.g., Google Fonts)
Root cause: dynamically generated stylesheets (Google Fonts varies the response based on User-Agent) cannot be pre-hashed. Fix: either self-host the font files and hash them, or remove the stylesheet from require-sri-for scope while keeping a strict style-src allowlist for that specific origin.
verify-sri-manifest.js fails in CI but passes locally
Root cause: non-deterministic build output — the bundler is embedding a build timestamp, random content hash suffix, or source-map URL that changes between runs. Fix: configure your bundler to produce deterministic output (Webpack output.filename with [contenthash] is fine but the contenthash itself must be stable given identical inputs; disable Date/Math.random() calls in build plugins).
SRI hash is correct but the resource still fails in Firefox
Root cause: Firefox enforces that the crossorigin attribute value matches the CORS mode the server permits. If the server returns Access-Control-Allow-Origin: * but the tag uses crossorigin="use-credentials", Firefox rejects the resource. Fix: use crossorigin="anonymous" for public CDN assets; use crossorigin="use-credentials" only for credentialed same-origin requests where the server explicitly allows credentials in CORS headers.
Security Boundary: What This Workflow Does Not Cover
Permalink to "Security Boundary: What This Workflow Does Not Cover"This risk assessment framework addresses the verification of known, enumerated dependencies. It does not protect against:
- Build-pipeline injection attacks targeting your CI/CD environment itself. If an attacker can modify your
generate-sri-manifest.jsor theexpected-sri.jsonfile in your repository, they can silently update both the asset and the expected hash. Protect against this with signed commits, CODEOWNERS restrictions on critical files, and Provenance Verification Workflows. - DOM-based XSS that dynamically creates
<script>tags at runtime. SRI only validates resources loaded through the standard HTML parser orfetch()with SRI options —document.createElement('script')calls bypass the integrity check unless your code explicitly sets theintegrityproperty before appending the element. - Data exfiltration via
fetch()orXMLHttpRequestto third-party endpoints. These are covered by CSPconnect-src, not by SRI. - npm registry account takeover if an attacker publishes a new version of a package you depend on. The lockfile prevents silent version upgrades in
npm ci, but you must still monitor for new malicious versions usingnpm auditand your Dependency Pinning Best Practices.
FAQ
Permalink to "FAQ"What is the minimum viable third-party risk assessment for a small team?
At minimum: commit a lockfile, run npm ci (not npm install) in CI, and add integrity + crossorigin="anonymous" to every CDN-hosted <script> and <link> tag. This covers the two most common supply-chain attack vectors — a compromised npm package and a tampered CDN asset — with zero additional tooling. A full trust-tier programme with SBOMs and violation monitoring is the mature target, but the minimum viable set is achievable in under a day.
How often should SRI hashes be rotated?
Hashes do not “expire” — they are recomputed whenever the asset changes. Practically, rotate the hash whenever: (a) the CDN vendor releases a new version of the library; (b) you intentionally upgrade the pinned version; © your runtime monitoring detects a violation against a known-good hash, which may indicate the asset has been modified. Define an SLA in your runbook — P1 violations (active mismatch) warrant a same-day emergency deploy; planned version upgrades follow your normal release cadence.
Can SRI be applied to dynamically injected scripts?
Yes, but it requires code-level handling. When you create a <script> element via document.createElement('script'), set the integrity property to the precomputed hash and the crossOrigin property to 'anonymous' before assigning src. The browser will then enforce the integrity check at load time. See Implementing Dynamic Script Loaders with Integrity for the complete implementation pattern.
Does using SRI hashes create a maintenance burden?
There is a real maintenance cost — hashes must be updated whenever an asset changes, and forgetting this breaks the site for real users (the browser blocks the script). The solution is automation: generate hashes in the CI pipeline, store them in a manifest, and have the template engine stamp every <script> tag from the manifest. Once the pipeline is in place, updating a CDN version becomes: update the URL → CI regenerates the hash → commit the updated manifest → deploy. The marginal cost per update is a single commit.
How do SBOMs relate to third-party risk assessments?
An SBOM is a structured, machine-readable serialisation of your dependency inventory with verified hashes. It transforms the risk assessment from a spreadsheet into a format that compliance frameworks (PCI DSS 6.3.2, NIST SSDF) can reference and that vulnerability-scanning tools can ingest automatically. Generating an SBOM at build time gives you a timestamped, signed record of exactly which components were in production at any given deployment — invaluable during incident response.
Related
Permalink to "Related"- Supply Chain Auditing & Dependency Verification — parent overview covering the full supply-chain security programme
- Lockfile Mapping & Analysis — resolving the full transitive dependency graph from lockfile data
- Dependency Pinning Best Practices — strategies for freezing exact versions across npm, pnpm, and Yarn
- Automated SBOM Generation — producing CycloneDX/SPDX SBOMs that capture verified hash provenance
- Provenance Verification Workflows — validating Sigstore signatures and build provenance attestations for npm packages