Search criteria
Related vulnerabilities
GHSA-898C-Q2CR-XWHG
Vulnerability from github – Published: 2026-05-29 15:54 – Updated: 2026-05-29 15:54Summary
axios 1.15.2 exposes two read-side prototype-pollution gadgets. When Object.prototype is polluted by an upstream dependency in the same process (e.g. lodash _.merge / CVE-2018-16487), axios silently picks up the polluted values:
- Header injection -
lib/utils.jsline 406 buildsmerge()'s accumulator asresult = {}, soresult[targetKey](line 414) walksObject.prototypeand the polluted bucket's own keys are copied into the merged headers and ride out on the wire. - Crash DoS -
lib/core/mergeConfig.jsline 26 builds thehasOwnPropertydescriptor as a plain-object literal.Object.definePropertyreadsdescriptor.get/descriptor.setvia the prototype chain, so a pollutedObject.prototype.getorObject.prototype.setmakes the call throwTypeErrorsynchronously on every axios request.
Affected Properties
| Polluted slot | Effect |
|---|---|
Object.prototype.common |
injects headers on every method |
Object.prototype.delete / .head / .post / .put / .patch / .query |
injects headers on the matching method |
Object.prototype.get |
every axios request throws TypeError: Getter must be a function from mergeConfig.js:26 |
Object.prototype.set |
every axios request throws TypeError: Setter must be a function from mergeConfig.js:26 |
Per-request headers (axios.request(url, { headers: {...} })) overwrite polluted entries. Polluting Object.prototype.get triggers the crash before any header is built.
Proof of Concept
const axios = require('axios');
// Finding A - header injection
Object.prototype.common = { 'X-Poisoned': 'yes' };
await axios.get('http://api.example.com/users');
// Wire request carries `X-Poisoned: yes`.
// Finding B - crash DoS
Object.prototype.get = { something: 'anything' };
await axios.get('http://api.example.com/users');
// TypeError: Getter must be a function: #<Object>
// at Function.defineProperty (<anonymous>)
// at mergeConfig (lib/core/mergeConfig.js:26:10)
Impact
- Server hang (
Content-Length: 99999): receiver waits for a body that never arrives. Affects requests with a body. - CL+TE conflict (
Transfer-Encoding: chunkedrides alongside axios's autoContent-Length): receiver rejects with400 Bad Request. Affects requests with a body. - Response suppression (
If-None-Match: *): receiver returns empty304 Not Modified. Affects GET / HEAD. - Crash DoS (
Object.prototype.get/.set): every axios request fails synchronously withTypeError, notAxiosError, so handlers filtering onerror.isAxiosErrormishandle the failure.
Attack Flow
flowchart TD
ROOT["Polluted Object.prototype<br/>via upstream gadget (e.g. lodash <= 4.17.10 _.merge / CVE-2018-16487)<br/>axios <= 1.15.2"]
ROOT --> CLASS_A["A. Arbitrary HTTP Header Injection<br/>Polluted defaults.headers slot rides along on every outbound axios request"]
ROOT --> CLASS_B["B. Crash DoS via Object.prototype.get / .set<br/>Polluted descriptor breaks Object.defineProperty in mergeConfig"]
CLASS_A --> PRE_A["Precondition: header not set per-request by the app<br/>Injected via defaults.headers slot<br/>(common, delete, head, post, put, patch, query)"]
PRE_A --> PA1["Response Suppression<br/>Trigger: common = {If-None-Match: *}<br/>Affects GET / HEAD"]
PA1 --> SA1["DoS<br/>304 Not Modified empty"]
PRE_A --> PA2["Server Hang<br/>Trigger: common = {Content-Length: 99999}<br/>Affects requests with body"]
PA2 --> SA2["DoS<br/>connection hang"]
PRE_A --> PA3["CL+TE Conflict<br/>Trigger: common = {Transfer-Encoding: chunked}<br/>Affects requests with body"]
PA3 --> SA3["DoS<br/>400 Bad Request"]
CLASS_B --> SB1["DoS<br/>TypeError: Getter / Setter must be a function<br/>Crashes every axios request, not only GET"]
%% Styles
style ROOT fill:#f87171,stroke:#991b1b,color:#fff
style CLASS_A fill:#fb923c,stroke:#9a3412,color:#fff
style CLASS_B fill:#fb923c,stroke:#9a3412,color:#fff
style PRE_A fill:#e2e8f0,stroke:#64748b,color:#1e293b
style PA1 fill:#fbbf24,stroke:#92400e,color:#000
style PA2 fill:#fbbf24,stroke:#92400e,color:#000
style PA3 fill:#fbbf24,stroke:#92400e,color:#000
style SA1 fill:#ef4444,stroke:#991b1b,color:#fff
style SA2 fill:#ef4444,stroke:#991b1b,color:#fff
style SA3 fill:#ef4444,stroke:#991b1b,color:#fff
style SB1 fill:#ef4444,stroke:#991b1b,color:#fff
Root Cause
Finding A. lib/utils.js:404-429's merge() creates result = {} at line 406. The dangerous-keys filter on lines 408-411 blocks the write side, but the read at line 414 (isPlainObject(result[targetKey])) still walks the prototype chain. When targetKey matches a polluted slot, result[targetKey] returns the polluted nested object, and the recursive merge(result[targetKey], val) on line 415 iterates that object's own keys via forEach and copies them as own properties into the new accumulator. Those keys flow through mergeConfig.js:35 → Axios.js:148 (utils.merge(headers.common, headers[config.method])) → Axios.js:155 (AxiosHeaders.concat(...)) → onto the wire via http.js:677 (headers: headers.toJSON()) → http.js:767 (transport.request(options, ...)).
Finding B. lib/core/mergeConfig.js:25 correctly makes config = Object.create(null), but the descriptor passed on line 26 is a plain-object literal - its get/set lookups walk Object.prototype. A polluted non-function Object.prototype.get or .set makes Object.defineProperty throw TypeError: Getter must be a function (or Setter must be a function) before the call returns. The descriptor is built unconditionally on every mergeConfig invocation, so every axios request throws - POST, PUT, DELETE, PATCH, HEAD, QUERY, not only GET.
Suggested Fix
Use null-prototype objects in place of the plain-object literals at lib/utils.js:406 and lib/core/mergeConfig.js:26-31. The same descriptor pattern recurs at lib/core/AxiosError.js:37, lib/core/AxiosHeaders.js:100, lib/utils.js:447/454/492/498, and lib/adapters/adapters.js:28/32.
Resources
- CVE-2018-16487 -
lodash.mergeprototype pollution inlodash <= 4.17.10 - CWE-1321 - Improperly Controlled Modification of Object Prototype Attributes
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "1.16.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.31.1"
},
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.32.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44490"
],
"database_specific": {
"cwe_ids": [
"CWE-1321"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-29T15:54:57Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\naxios `1.15.2` exposes two read-side prototype-pollution gadgets. When `Object.prototype` is polluted by an upstream dependency in the same process (e.g. lodash `_.merge` / [CVE-2018-16487](https://nvd.nist.gov/vuln/detail/CVE-2018-16487)), axios silently picks up the polluted values:\n\n1. **Header injection** - `lib/utils.js` line 406 builds `merge()`\u0027s accumulator as `result = {}`, so `result[targetKey]` (line 414) walks `Object.prototype` and the polluted bucket\u0027s own keys are copied into the merged headers and ride out on the wire.\n2. **Crash DoS** - `lib/core/mergeConfig.js` line 26 builds the `hasOwnProperty` descriptor as a plain-object literal. `Object.defineProperty` reads `descriptor.get`/`descriptor.set` via the prototype chain, so a polluted `Object.prototype.get` or `Object.prototype.set` makes the call throw `TypeError` synchronously on every axios request.\n\n## Affected Properties\n\n| Polluted slot | Effect |\n|---|---|\n| `Object.prototype.common` | injects headers on every method |\n| `Object.prototype.delete` / `.head` / `.post` / `.put` / `.patch` / `.query` | injects headers on the matching method |\n| `Object.prototype.get` | every axios request throws `TypeError: Getter must be a function` from `mergeConfig.js:26` |\n| `Object.prototype.set` | every axios request throws `TypeError: Setter must be a function` from `mergeConfig.js:26` |\n\nPer-request headers (`axios.request(url, { headers: {...} })`) overwrite polluted entries. Polluting `Object.prototype.get` triggers the crash before any header is built.\n\n## Proof of Concept\n\n```javascript\nconst axios = require(\u0027axios\u0027);\n\n// Finding A - header injection\nObject.prototype.common = { \u0027X-Poisoned\u0027: \u0027yes\u0027 };\nawait axios.get(\u0027http://api.example.com/users\u0027);\n// Wire request carries `X-Poisoned: yes`.\n\n// Finding B - crash DoS\nObject.prototype.get = { something: \u0027anything\u0027 };\nawait axios.get(\u0027http://api.example.com/users\u0027);\n// TypeError: Getter must be a function: #\u003cObject\u003e\n// at Function.defineProperty (\u003canonymous\u003e)\n// at mergeConfig (lib/core/mergeConfig.js:26:10)\n```\n\n## Impact\n\n- **Server hang** (`Content-Length: 99999`): receiver waits for a body that never arrives. Affects requests with a body.\n- **CL+TE conflict** (`Transfer-Encoding: chunked` rides alongside axios\u0027s auto `Content-Length`): receiver rejects with `400 Bad Request`. Affects requests with a body.\n- **Response suppression** (`If-None-Match: *`): receiver returns empty `304 Not Modified`. Affects GET / HEAD.\n- **Crash DoS** (`Object.prototype.get` / `.set`): every axios request fails synchronously with `TypeError`, not `AxiosError`, so handlers filtering on `error.isAxiosError` mishandle the failure.\n\n## Attack Flow\n\n```mermaid\nflowchart TD\n ROOT[\"Polluted Object.prototype\u003cbr/\u003evia upstream gadget (e.g. lodash \u0026lt;= 4.17.10 _.merge / CVE-2018-16487)\u003cbr/\u003eaxios \u0026lt;= 1.15.2\"]\n\n ROOT --\u003e CLASS_A[\"A. Arbitrary HTTP Header Injection\u003cbr/\u003ePolluted defaults.headers slot rides along on every outbound axios request\"]\n ROOT --\u003e CLASS_B[\"B. Crash DoS via Object.prototype.get / .set\u003cbr/\u003ePolluted descriptor breaks Object.defineProperty in mergeConfig\"]\n\n CLASS_A --\u003e PRE_A[\"Precondition: header not set per-request by the app\u003cbr/\u003eInjected via defaults.headers slot\u003cbr/\u003e(common, delete, head, post, put, patch, query)\"]\n\n PRE_A --\u003e PA1[\"Response Suppression\u003cbr/\u003eTrigger: common = {If-None-Match: *}\u003cbr/\u003eAffects GET / HEAD\"]\n PA1 --\u003e SA1[\"DoS\u003cbr/\u003e304 Not Modified empty\"]\n\n PRE_A --\u003e PA2[\"Server Hang\u003cbr/\u003eTrigger: common = {Content-Length: 99999}\u003cbr/\u003eAffects requests with body\"]\n PA2 --\u003e SA2[\"DoS\u003cbr/\u003econnection hang\"]\n\n PRE_A --\u003e PA3[\"CL+TE Conflict\u003cbr/\u003eTrigger: common = {Transfer-Encoding: chunked}\u003cbr/\u003eAffects requests with body\"]\n PA3 --\u003e SA3[\"DoS\u003cbr/\u003e400 Bad Request\"]\n\n CLASS_B --\u003e SB1[\"DoS\u003cbr/\u003eTypeError: Getter / Setter must be a function\u003cbr/\u003eCrashes every axios request, not only GET\"]\n\n %% Styles\n style ROOT fill:#f87171,stroke:#991b1b,color:#fff\n style CLASS_A fill:#fb923c,stroke:#9a3412,color:#fff\n style CLASS_B fill:#fb923c,stroke:#9a3412,color:#fff\n style PRE_A fill:#e2e8f0,stroke:#64748b,color:#1e293b\n style PA1 fill:#fbbf24,stroke:#92400e,color:#000\n style PA2 fill:#fbbf24,stroke:#92400e,color:#000\n style PA3 fill:#fbbf24,stroke:#92400e,color:#000\n style SA1 fill:#ef4444,stroke:#991b1b,color:#fff\n style SA2 fill:#ef4444,stroke:#991b1b,color:#fff\n style SA3 fill:#ef4444,stroke:#991b1b,color:#fff\n style SB1 fill:#ef4444,stroke:#991b1b,color:#fff\n```\n\n## Root Cause\n\n**Finding A.** `lib/utils.js:404-429`\u0027s `merge()` creates `result = {}` at line 406. The dangerous-keys filter on lines 408-411 blocks the write side, but the read at line 414 (`isPlainObject(result[targetKey])`) still walks the prototype chain. When `targetKey` matches a polluted slot, `result[targetKey]` returns the polluted nested object, and the recursive `merge(result[targetKey], val)` on line 415 iterates that object\u0027s own keys via `forEach` and copies them as own properties into the new accumulator. Those keys flow through `mergeConfig.js:35` \u2192 `Axios.js:148` (`utils.merge(headers.common, headers[config.method])`) \u2192 `Axios.js:155` (`AxiosHeaders.concat(...)`) \u2192 onto the wire via `http.js:677` (`headers: headers.toJSON()`) \u2192 `http.js:767` (`transport.request(options, ...)`).\n\n**Finding B.** `lib/core/mergeConfig.js:25` correctly makes `config = Object.create(null)`, but the descriptor passed on line 26 is a plain-object literal - its `get`/`set` lookups walk `Object.prototype`. A polluted non-function `Object.prototype.get` or `.set` makes `Object.defineProperty` throw `TypeError: Getter must be a function` (or `Setter must be a function`) before the call returns. The descriptor is built unconditionally on every `mergeConfig` invocation, so every axios request throws - POST, PUT, DELETE, PATCH, HEAD, QUERY, not only GET.\n\n## Suggested Fix\n\nUse null-prototype objects in place of the plain-object literals at `lib/utils.js:406` and `lib/core/mergeConfig.js:26-31`. The same descriptor pattern recurs at `lib/core/AxiosError.js:37`, `lib/core/AxiosHeaders.js:100`, `lib/utils.js:447/454/492/498`, and `lib/adapters/adapters.js:28/32`.\n\n## Resources\n\n- [CVE-2018-16487](https://nvd.nist.gov/vuln/detail/CVE-2018-16487) - `lodash.merge` prototype pollution in `lodash \u003c= 4.17.10`\n- [CWE-1321](https://cwe.mitre.org/data/definitions/1321.html) - Improperly Controlled Modification of Object Prototype Attributes",
"id": "GHSA-898c-q2cr-xwhg",
"modified": "2026-05-29T15:54:57Z",
"published": "2026-05-29T15:54:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/axios/axios/security/advisories/GHSA-898c-q2cr-xwhg"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2018-16487"
},
{
"type": "PACKAGE",
"url": "https://github.com/axios/axios"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "axios has DoS \u0026 Header Injection via Prototype Pollution Read-Side Gadgets in axios merge functions"
}