The Exploit
Attacker needs only the ability to inject a malicious sponsor record into process-sponsors.js during the docs build process.
node - <<'NODE'
const allSponsorsProcessedData = [
{ slug: 'evil', tier: '__proto__', url: 'javascript:alert(1)' }
];
const activeSponsorsProcessedData = [];
const buildLinks = (url, bypass = false) => {
if (bypass) return url;
try {
const urlObject = new URL(url);
const { searchParams } = urlObject;
searchParams.set('utm_source', 'axios_docs_website');
searchParams.set('utm_medium', 'website');
searchParams.set('utm_campaign', 'axios_open_collective_sponsorship');
return urlObject.toString();
} catch {
return url;
}
};
const sponsorsByTier = {};
for (const sponsor of allSponsorsProcessedData) {
sponsorsByTier[sponsor.tier] ||= [];
}
console.log('poisoned prototype push available:', typeof ({}).push);
console.log('link output:', buildLinks(allSponsorsProcessedData[0].url));
NODE
The output shows the bug in action: a plain object now inherits array methods because sponsor.tier was __proto__, and the link builder returns a javascript: URI with tracking parameters appended.
What the Patch Did
Before:
const buildLinks = (url, bypass = false) => {
if (bypass) {
return url;
}
try {
const urlObject = new URL(url);
const { searchParams } = urlObject;
searchParams.set('utm_source', 'axios_docs_website');
searchParams.set('utm_medium', 'website');
searchParams.set('utm_campaign', 'axios_open_collective_sponsorship');
return urlObject.toString();
} catch {
return url;
}
};
const sponsorsByTier = {};
for (const sponsor of allSponsorsProcessedData) {
sponsorsByTier[sponsor.tier] ||= [];
...
}
After:
const buildLinks = (url, bypass = false) => {
if (bypass) {
return url;
}
try {
const urlObject = new URL(url);
if (!['http:', 'https:'].includes(urlObject.protocol)) {
return null;
}
const { searchParams } = urlObject;
searchParams.set('utm_source', 'axios_docs_website');
searchParams.set('utm_medium', 'website');
searchParams.set('utm_campaign', 'axios_open_collective_sponsorship');
return urlObject.toString();
} catch {
return null;
}
};
const sponsorsByTier = Object.create(null);
for (const sponsor of allSponsorsProcessedData) {
sponsorsByTier[sponsor.tier] ||= [];
...
}
The patch added two precise controls: a protocol whitelist for parsed URLs, and a null-prototype dictionary to prevent attacker-controlled keys from mutating Object.prototype.
Root Cause
The bug combined two failures in docs/scripts/process-sponsors.js. First, buildLinks() accepted any url scheme that new URL(url) could parse, so attacker-controlled input like javascript:alert(1) survived and was returned as an href candidate. Second, the sponsor grouping code used a plain object literal for sponsorsByTier and indexed it with attacker-controlled sponsor.tier; a value of __proto__ crossed the trust boundary and mutated the prototype chain instead of creating an ordinary map entry. This is prototype pollution (CWE-1321) plus insufficient input validation for URL schemes (CWE-20).
Why It Works
The load-bearing fixes are the protocol check and the Object.create(null) line. Without if (!['http:', 'https:'].includes(urlObject.protocol)), javascript: remains a valid URL object and the helper still returns it, so the URL validation bug is still exploitable. Without Object.create(null), sponsorsByTier[sponsor.tier] ||= [] on a malicious __proto__ key would again alter object inheritance. The return null changes are the safe failure path needed after introducing the protocol check; they ensure invalid or disallowed URLs are dropped instead of echoed back.
Hardening Checklist
- Use
Object.create(null)for maps keyed by external data instead of{}. - Validate
URL.protocolagainst a whitelist like['http:', 'https:']after parsing withnew URL(). - Fail closed: return
nullor throw on parse failures instead of returning the original untrusted string. - Avoid shorthand object initialization (
obj[key] ||= []) whenkeycan be attacker-controlled; use explicit key checks likeObject.hasOwn(obj, key)if needed. - Normalize third-party metadata before template building so fields like
tierandurlare constrained to expected values.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-40175