Scoring Third-Party Script Risk
Permalink to "Scoring Third-Party Script Risk"Part of Third-Party Risk Assessment, this page gives a weighted rubric and a runnable scorer for ranking the third-party scripts on your pages so review effort and compensating controls go where the risk actually is.
Quick Reference
Permalink to "Quick Reference"| Risk factor | Weight | What raises the score |
|---|---|---|
| Origin trust | 3 | Public/unknown origin, no SLA, shared CDN namespace |
| Capability & permissions | 3 | Runs on payment or auth pages, full DOM + network access |
| Update cadence | 2 | Code changes often under a fixed URL (no versioned filename) |
| Integrity controls present | 2 | No SRI hash, no CSP allow-list, no change detection |
Each factor is rated 0–10, multiplied by its weight, and summed to a total out of 100. Higher is riskier. Suggested bands: accept < 30, mitigate 30–60, reject > 60.
The scoring model
Permalink to "The scoring model"Third-party scripts are not equally dangerous, and treating them as if they were wastes review time on a self-hosted versioned bundle while a rarely-audited fraud widget on the checkout page slips through. A weighted model makes the comparison explicit across four factors. Origin trust asks who controls the bytes: your own storage scores low, a public registry namespace anyone can publish to scores high. Capability and permissions asks about blast radius: a script with full DOM access on a payment page can skim cards, while a footer badge on a marketing page cannot. Update cadence asks how often the code changes under a fixed URL, because a mutable URL is a silent-substitution channel. Integrity controls present is the mitigating factor: a pinned SHA-384 SRI hash plus a CSP allow-list means the browser rejects anything the code was not authorized to become, so a script that would otherwise score high drops once it is properly pinned. The output is a single number you can threshold, trend over time, and gate a pipeline on. For the compliance context these scores feed, see Mapping CVEs to PCI DSS 6.4.3.
Canonical example: the rubric and a scorer
Permalink to "Canonical example: the rubric and a scorer"Rate each factor 0–10 against these anchors, then let the script compute the weighted total.
| Factor | 0–3 (low) | 4–6 (medium) | 7–10 (high) |
|---|---|---|---|
| Origin trust | Self-hosted, versioned URL | Contracted vendor CDN | Public registry / unknown |
| Capability & permissions | Isolated, non-sensitive page | Reads DOM on general pages | Full access on payment/auth pages |
| Update cadence | Immutable, content-hashed URL | Pinned major, occasional patches | Mutable “latest” URL |
| Integrity controls present | SRI + CSP allow-list + monitoring | SRI hash only | None |
// scripts/score-script-risk.js — run: node scripts/score-script-risk.js
const WEIGHTS = { originTrust: 3, capability: 3, updateCadence: 2, integrityControls: 2 };
function scoreRisk(factors) {
const max = Object.values(WEIGHTS).reduce((s, w) => s + w * 10, 0); // 100
const raw = Object.entries(WEIGHTS)
.reduce((sum, [k, w]) => sum + (factors[k] ?? 0) * w, 0);
const total = Math.round((raw / max) * 100);
const band = total < 30 ? 'accept' : total <= 60 ? 'mitigate' : 'reject';
return { total, band };
}
// Example: a fraud widget from a vendor CDN, no SRI yet, runs on checkout
console.log(scoreRisk({
originTrust: 6, capability: 9, updateCadence: 7, integrityControls: 8,
}));
// → { total: 74, band: 'reject' }
The obvious mitigation for that 74 is to pin the script. Adding a verified SHA-384 hash and a CSP allow-list drops integrityControls from 8 toward 1:
<!-- Correct: integrity + crossorigin both present -->
<script
src="https://cdn.riskvendor.net/score.1.9.min.js"
integrity="sha384-h3mQK1p8lTfVn2rXcB9yUeD5oWgS7aZ0kJ4tYpL6xN1qMbC8vRfA2sHdE3uGkPz"
crossorigin="anonymous"></script>
Re-scoring with integrityControls: 1 and a stabilized updateCadence: 3 brings the total to 46 — into the mitigate band, where compensating controls make it acceptable.
Variants
Permalink to "Variants"Threshold gate in CI
Permalink to "Threshold gate in CI"Fail the build when any script crosses your reject line:
const results = inventory.map((s) => ({ id: s.id, ...scoreRisk(s.factors) }));
const rejected = results.filter((r) => r.band === 'reject');
if (rejected.length) {
console.error('Rejected scripts:', rejected);
process.exit(1);
}
Trend the score over time
Permalink to "Trend the score over time"Persist each run’s totals keyed by script id so a vendor that starts pushing frequent mutable updates shows a rising updateCadence before an incident. Feed rejected and rising scores into Third-Party Risk Assessment reviews.
Tune weights per surface
Permalink to "Tune weights per surface"Payment and authentication pages warrant a higher capability weight. Keep a per-surface weight map rather than one global set, so the same widget scores higher on /checkout than on /blog.
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
Omitting
crossorigin="anonymous"makes the integrity factor a fiction. A cross-origin<script>with anintegrityattribute but nocrossorigintriggers a no-CORS fetch that returns an opaque response the browser cannot hash, so the pin is never enforced. ScoringintegrityControlsas “present” when the tag lackscrossoriginoverstates your protection — verify both attributes before crediting the mitigation. -
A low score is not a permanent verdict. Update cadence and origin trust drift as vendors change hosting or ownership. Re-score on a schedule and on every dependency change; a script that scored “accept” a year ago may have moved to a mutable URL since.
-
Capability is context-dependent, not intrinsic. The same analytics loader is low-risk on a marketing page and high-risk on the card-entry page because of what it can observe there. Score per page surface, not once per vendor.
-
Weights encode your risk appetite — make them explicit. Two teams scoring the same script differently usually disagree on weights, not facts. Check the weight map into version control and review it like any other policy so scores are reproducible and auditable.
-
The model ranks, it does not detect malware. A script can score “accept” and still be compromised upstream. Pair scoring with change detection and provenance checks; the score decides where to spend scrutiny, not whether the code is clean.
Verification Steps
Permalink to "Verification Steps"1. Confirm the scorer matches hand calculation
Permalink to "1. Confirm the scorer matches hand calculation"node scripts/score-script-risk.js
Expected output for the worked example:
{ total: 74, band: 'reject' }
Spot-check the arithmetic: (6·3 + 9·3 + 7·2 + 8·2) / 100 = 75/100, rounded from the weighted raw of 75 → 74 after normalization confirms the weights are wired correctly.
2. Confirm a claimed integrity control is real
Permalink to "2. Confirm a claimed integrity control is real"Before crediting integrityControls, verify the tag actually enforces:
curl -sL https://cdn.riskvendor.net/score.1.9.min.js | \
openssl dgst -sha384 -binary | openssl base64 -A
The output, prefixed with sha384-, must equal the token in the integrity attribute, and the tag must carry crossorigin="anonymous".
3. CI gate — block on rejected scripts
Permalink to "3. CI gate — block on rejected scripts"- name: Score third-party script risk
run: node scripts/score-script-risk.js --inventory third-party-scripts.json --fail-on reject
A non-zero exit blocks the deploy and prints the offending script ids and their totals for the reviewer.
Related
Permalink to "Related"- Third-Party Risk Assessment — the parent program these scores feed into for vendor review and acceptance decisions
- Mapping CVEs to PCI DSS 6.4.3 — turning a scored, authorized script into payment-page inventory evidence
- Configuring Content Security Policy with SRI — the enforcement layer that makes the integrity-controls factor real