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: 75, band: 'reject' }
The obvious mitigation for that 75 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 53 — into the mitigate band, where compensating controls make it acceptable. Notice which contributions did not move. Origin trust and capability describe the vendor relationship and the page surface; pinning a hash cannot touch either. Only the two factors that describe how the bytes reach the browser collapse, and between them they account for the whole 22-point drop. That is the useful property of a weighted model: it shows you not just that a control helped, but exactly which part of the exposure it retired and which part you are still carrying.
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 your vendor review queue. The signal that drives this trend is byte-level, not calendar-level, so pair it with Detecting Changes in Third-Party Scripts and let an observed hash change bump updateCadence automatically instead of waiting for a human to notice.
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. The rubric anchors stay identical; only the multiplier changes, which keeps the two scores comparable while still reflecting that the same DOM access buys an attacker far more on a card-entry form than under a blog post.
Where the checkout column keeps escalating, the durable fix is usually to cut capability rather than to argue about the weight: Self-Hosting Third-Party Scripts collapses origin trust and update cadence at the same time, because you then control both the bytes and the URL they are served from.
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. Capability is also the one factor you can engineer down rather than merely document: moving the tag into a cross-origin frame, as in Sandboxing Analytics Scripts with Iframes, removes its reach into the top-level DOM and legitimately drops the rating.
-
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: 75, band: 'reject' }
Spot-check the arithmetic by hand: 6·3 + 9·3 + 7·2 + 8·2 = 18 + 27 + 14 + 16 = 75. Because the weights sum to 10 and each factor is capped at 10, the maximum is exactly 100, so the raw total and the normalized total are the same number. If your printed total differs from the hand sum, the weight map in the script no longer matches the one you scored against — usually because a per-surface override was applied without being passed to the hand calculation.
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.
Frequently Asked Questions
Permalink to "Frequently Asked Questions"What factors belong in a third-party script risk score?
Four factors cover most cases: origin trust (who controls the serving origin), capability and permissions (the script’s blast radius), update cadence (how often the code changes under a fixed URL), and integrity controls present (whether an SRI hash and CSP already pin it). Weight each by impact and sum.
Does adding an SRI hash lower a script's risk score?
Yes. A pinned SHA-384 SRI hash means the browser blocks any tampered or silently updated version, which directly reduces the substitution and update-cadence risk. The integrity-controls factor should score lower once a verified hash and CSP allow-list are in place.
How do I turn a risk score into a decision?
Set thresholds. For example, accept below 30, require compensating controls between 30 and 60, and reject above 60. Wire the scorer into CI so a new or changed script that crosses the threshold blocks the build until it is reviewed.
How often should a third-party script be re-scored?
Re-score on every dependency or tag-manager change, and on a fixed schedule of at least once a quarter for everything else. Origin trust and update cadence drift as vendors change hosting, ownership, or release habits, and a score computed against last year’s facts is evidence of nothing. Automated change detection can trigger a re-score the moment the served bytes move.
Should a script that fails the score be removed or just mitigated?
Mitigation first, removal when mitigation is impossible. Pinning a SHA-384 hash with crossorigin anonymous, moving to an immutable versioned URL, self-hosting the file, or confining the script to a sandboxed iframe all lower real factors rather than the paperwork. Remove the script only when the vendor cannot serve a stable, pinnable artifact.
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
- Detecting Changes in Third-Party Scripts — the byte-level monitoring that keeps the update-cadence factor honest between reviews