2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

docs: update threatmodel (#10765)

* docs: update threatmodel

* Update THREATMODEL.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Jay
2026-04-19 18:38:36 +02:00
committed by GitHub
parent f93f815525
commit 908f2206b6
+78 -54
View File
@@ -111,17 +111,17 @@ The runtime model is general by design - axios is a transport library and cannot
#### T-R3: Header injection (CRLF)
| | |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Application puts user input into a header value: `headers: { 'X-User': req.query.name }`. Attacker supplies `foo\r\nX-Injected: bar\r\n\r\n<body>`. |
| **Likelihood** | Low |
| **Impact** | MediumHigh (request smuggling, response splitting) |
| **Mitigations** | `lib/core/AxiosHeaders.js` rejects header values containing `\r` or `\n`, and validates header names against an RFC-7230-shaped charset. Node's own `http` module also rejects these. |
| **Residual risk** | Very low. Defense in depth (axios + Node). |
| | |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Application puts user input into a header value: `headers: { 'X-User': req.query.name }`. Attacker supplies `foo\r\nX-Injected: bar\r\n\r\n<body>`. A related surface is **multipart per-part headers** — attacker-controlled `blob.type` or `blob.name` flowing into the multipart body. |
| **Likelihood** | Low |
| **Impact** | MediumHigh (request smuggling, response splitting, multipart parser confusion) |
| **Mitigations** | `lib/core/AxiosHeaders.js` rejects header values containing `\r` or `\n`, and validates header names against an RFC-7230-shaped charset. Node's own `http` module also rejects these. <br>• `lib/helpers/formDataToStream.js` strips CRLF from `value.type` and percent-encodes CRLF/`"` in `value.name` via `escapeName()` before interpolating them into per-part headers (GHSA-445q-vr5w-6q77). Node's `http` module does **not** defend here — multipart injection is in body bytes, not request headers. |
| **Residual risk** | Very low for HTTP headers (defense in depth: axios + Node). Low for multipart body headers (single layer of defense; regressions here would be silent). |
---
#### T-R4: Prototype pollution via response body
#### T-R4: Prototype pollution — write side (polluting response / merge into a target object)
| | |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -133,28 +133,40 @@ The runtime model is general by design - axios is a transport library and cannot
---
#### T-R4b: Prototype pollution — read-side gadgets (polluted `Object.prototype` drives axios behavior)
| | |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | A _different_ library in the caller's dependency tree pollutes `Object.prototype` (e.g. `Object.prototype.validateStatus = () => true`). axios code that reads a config property through the prototype chain then picks up the attacker's value and executes the associated behavior. Each reachable property is a distinct **gadget**: `validateStatus` (bypass HTTP error handling), `parseReviver` (silently tamper JSON response bodies), `transport` / `httpAgent` / `lookup` (MITM / intercept), `withXSRFToken` (leak XSRF token cross-origin), `transformResponse` (response replacement), and so on. |
| **Likelihood** | LowMedium (requires a polluted prototype somewhere in the process — historically common). |
| **Impact** | High — arbitrary behavior change across every axios call (auth bypass, response tampering, credential leakage). Unlike T-R4, this does not require axios itself to pollute — any polluted process is enough. |
| **Mitigations** | Config reads that can drive behavior are routed through `hasOwnProp` guards so polluted prototype properties are not seen: <br>• `lib/core/mergeConfig.js` — per-prop reads from `config1`/`config2` guarded with `hasOwnProp`; `mergeDirectKeys` (used by `validateStatus`) uses `hasOwnProp` rather than the `in` operator which traverses the prototype chain (fix for GHSA-w9j2-pvgh-6h63). <br>• `lib/defaults/index.js``transformResponse` / `transformRequest` read `transitional`, `responseType`, `parseReviver`, `response` via an `own()` wrapper (fix for GHSA-3w6x-2g7m-8v23). <br>• `lib/adapters/http.js``transport`, `httpAgent`, `httpsAgent`, `lookup`, `family`, `http2Options`, etc. read via `hasOwnProp` (fix for GHSA-pf86-5x62-jrwf gadget set). <br>• `lib/helpers/resolveConfig.js``withXSRFToken` requires strict `=== true` to send the header cross-origin; non-boolean truthy values (`1`, `"false"`, `{}`) no longer short-circuit the same-origin check (fix for GHSA-xx6v-rp6x-q39c). <br>• Regression tests for the gadget class live in `tests/unit/prototypePollution.test.js` (both unit-level and end-to-end against `axios.get`). |
| **Residual risk** | Low, but the surface is _every config property read_. Any new code path that reads `config.foo` / `this.foo` / destructures from a merged config MUST use a `hasOwnProp` guard. The non-goal that axios does not defend a caller with a polluted prototype is **narrower than it sounds** — the pollution typically comes from a transitive dep, not from the caller's own intent, and the above mitigations _do_ neutralize the reachable gadgets even when the prototype is polluted. |
---
#### T-R5: Decompression bomb
| | |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Server sends `Content-Encoding: gzip` with a 10 KB body that decompresses to 10 GB. |
| **Likelihood** | Low |
| **Impact** | Medium (DoS - OOM kill of the calling process) |
| **Mitigations** | • `maxContentLength` bounds the **decompressed** response size in the Node adapter (`lib/adapters/http.js`). <br>• `maxBodyLength` bounds the request side. <br>• Both default to `-1` (unlimited). **Callers handling untrusted servers should set these.** <br>• Decompression uses Node's `zlib`, which streams - memory is bounded by the limit, not the full expansion. |
| **Residual risk** | Medium when limits are not configured. The defaults favor compatibility over safety. |
| | |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Server sends `Content-Encoding: gzip` with a 10 KB body that decompresses to 10 GB. |
| **Likelihood** | Low |
| **Impact** | Medium (DoS - OOM kill of the calling process) |
| **Mitigations** | • `maxContentLength` bounds the **decompressed** response size in the Node adapter (`lib/adapters/http.js`), enforced chunk-by-chunk on the decompressed stream for both buffered and `responseType: 'stream'` responses (stream path fixed in GHSA-vf2m-468p-8v99). <br>• `maxBodyLength` bounds the request side, including when `maxRedirects === 0` (previously bypassed). <br>• Both default to `-1` (unlimited). **Callers handling untrusted servers should set these.** The README carries a top-level "security notice" call-out and `docs/pages/misc/security.md` documents the exact mitigation snippet in all four locales. <br>• Decompression uses Node's `zlib`, which streams memory is bounded by the limit, not the full expansion. |
| **Residual risk** | Medium when limits are not configured. The defaults favor compatibility over safety; the reasoning is that tightening the default would silently break every legitimate download larger than whatever cap were chosen. |
---
#### T-R6: TLS validation bypass
| | |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| | |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Caller passes `httpsAgent: new https.Agent({ rejectUnauthorized: false })` to "fix" a certificate error in dev, ships it to prod. |
| **Likelihood** | Medium (very common copy-paste anti-pattern) |
| **Impact** | High (silent MITM) |
| **In scope?** | **No.** axios delegates TLS entirely to Node's `https` module / the browser. We do not inspect or warn on agent configuration. |
| **Mitigations** | None at the axios layer. Documentation responsibility only. |
| **Residual risk** | High, but explicitly out of scope. This is caller misconfiguration, not an axios vulnerability. |
| **Likelihood** | Medium (very common copy-paste anti-pattern) |
| **Impact** | High (silent MITM) |
| **In scope?** | **No.** axios delegates TLS entirely to Node's `https` module / the browser. We do not inspect or warn on agent configuration. |
| **Mitigations** | None at the axios layer. Documentation responsibility only. |
| **Residual risk** | High, but explicitly out of scope. This is caller misconfiguration, not an axios vulnerability. |
---
@@ -184,13 +196,13 @@ The runtime model is general by design - axios is a transport library and cannot
#### T-R9: Proxy environment variable hijack
| | |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Attacker controls the process environment (compromised CI step, container escape, `.env` injection) and sets `HTTPS_PROXY=http://evil.com:8080`. All axios traffic is now MITM'd. |
| **Likelihood** | Low (requires prior foothold) |
| **Impact** | High |
| **Mitigations** | • `config.proxy: false` disables environment-based proxy detection entirely. <br>• `NO_PROXY` is honored (`lib/helpers/shouldBypassProxy.js`). <br>• HTTPS through an HTTP proxy still validates the origin's cert (CONNECT tunnel) - the proxy sees SNI but not plaintext. |
| **Residual risk** | Low for HTTPS. **High for plain HTTP** - the proxy sees and can modify everything. |
| | |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Description** | Attacker controls the process environment (compromised CI step, container escape, `.env` injection) and sets `HTTPS_PROXY=http://evil.com:8080`. All axios traffic is now MITM'd. |
| **Likelihood** | Low (requires prior foothold) |
| **Impact** | High |
| **Mitigations** | • `config.proxy: false` disables environment-based proxy detection entirely. <br>• `NO_PROXY` is honored (`lib/helpers/shouldBypassProxy.js`), with recent hardening for CIDR ranges, IPv6 literals, and wildcard patterns to close parser-differential edge cases. <br>• HTTPS through an HTTP proxy still validates the origin's cert (CONNECT tunnel) - the proxy sees SNI but not plaintext. |
| **Residual risk** | Low for HTTPS. **High for plain HTTP** - the proxy sees and can modify everything. |
---
@@ -206,6 +218,18 @@ The runtime model is general by design - axios is a transport library and cannot
---
#### T-R11: Form-data recursion DoS (deeply nested input)
| | |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Caller passes untrusted object input as request `data` in a context that serializes to `multipart/form-data` or `application/x-www-form-urlencoded`. A pathological input with thousands of nesting levels causes `lib/helpers/toFormData.js` to recurse until stack overflow or the process is killed. |
| **Likelihood** | Low (requires the caller to serialize attacker-controlled object input without validation) |
| **Impact** | Medium (DoS — stack overflow / process termination) |
| **Mitigations** | • `formSerializer.maxDepth` caps recursion depth; default is 100, can be set to `Infinity` to disable. <br>• Exceeding the cap throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` rather than crashing the process. <br>• Documented per locale in `docs/pages/advanced/multipart-form-data-format.md` and `docs/pages/advanced/x-www-form-urlencoded-format.md`. |
| **Residual risk** | Low when callers leave the default in place. Setting `maxDepth: Infinity` reintroduces the risk. |
---
### 2.6 Explicit Non-Goals (Runtime)
axios will **not**:
@@ -214,7 +238,7 @@ axios will **not**:
- Validate that `config.url` points somewhere "safe." We don't know what safe means for your application.
- Warn when TLS validation is disabled via a custom agent.
- Redact `config` from thrown errors - the caller may legitimately need it for retry logic.
- Defend against a caller that has already been compromised (e.g. polluted `Object.prototype` before calling axios).
- Defend against a fully compromised caller process (e.g. attacker-controlled code running inside the caller). Note: for the narrower case of a **polluted `Object.prototype` arriving via a transitive dependency**, axios _does_ defend the reachable config-read gadgets (see T-R4b) — but any new config-read path must continue to use `hasOwnProp` guards to stay on this side of the line.
---
@@ -293,13 +317,13 @@ This is the model that protects **what gets published as `axios` on npm**. A suc
#### T-S1: Malicious code in a contributor PR
| | |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Attacker opens a PR with a subtle backdoor - an obfuscated payload in a test fixture, a Unicode homoglyph in a comparison, a malicious `rollup` plugin in the config. |
| **Likelihood** | **High** (attempts are constant on high-profile repos) |
| **Impact** | Critical, _if_ it lands |
| **Mitigations** | • Mandatory review before merge. <br>• `pull_request` workflows run with no secrets and a read-only token - a malicious test cannot exfiltrate anything from CI. <br>• `pull_request_target` is **not** used (it would grant secrets to fork code). <br>• `zizmor` lints workflow files for known-dangerous patterns. <br>• Branch protection on `v1.x`. |
| **Gaps** | • Review is human and fallible. Obfuscated changes to `dist/` (if checked in) or to large test fixtures are hard to spot. <br>• No automated diffing of `lib/``dist/` to catch build-output tampering. <br>• No `CODEOWNERS` requiring specific reviewers for `lib/`, `.github/`, `rollup.config.js`. |
| | |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Attacker opens a PR with a subtle backdoor - an obfuscated payload in a test fixture, a Unicode homoglyph in a comparison, a malicious `rollup` plugin in the config. |
| **Likelihood** | **High** (attempts are constant on high-profile repos) |
| **Impact** | Critical, _if_ it lands |
| **Mitigations** | • Mandatory review before merge. <br>• `pull_request` workflows run with no secrets and a read-only token - a malicious test cannot exfiltrate anything from CI. <br>• `pull_request_target` is **not** used (it would grant secrets to fork code). <br>• `zizmor` lints workflow files for known-dangerous patterns. <br>• Branch protection on `v1.x`. |
| **Gaps** | • Review is human and fallible. Obfuscated changes to `dist/` (if checked in) or to large test fixtures are hard to spot. <br>• No automated diffing of `lib/``dist/` to catch build-output tampering. <br>• `.github/CODEOWNERS` exists but is a single catch-all (`* @jasonsaayman`). Path-scoped ownership for `lib/`, `.github/workflows/`, and `rollup.config.js` would route these sensitive changes through mandatory reviewers distinct from the catch-all. |
---
@@ -394,19 +418,19 @@ This is the model that protects **what gets published as `axios` on npm**. A suc
| **Likelihood** | Low |
| **Impact** | Critical |
| **Mitigations** | • All actions are pinned to **full commit SHAs**, not tags - `actions/checkout@de0fac` not `@v6`. A compromised action tag can't silently change behavior. ✅ <br>• `permissions:` are minimal (`contents: read`, `id-token: write`). <br>• `persist-credentials: false` on checkout - the build steps cannot push back to the repo. <br>• `zizmor` lints workflows on every PR. <br>• The `npm-publish` GitHub Environment can require designated reviewers before the job runs - a tampered workflow still pauses for human approval. |
| **Gaps** | • No `CODEOWNERS` rule requiring extra scrutiny on `.github/workflows/**`. A workflow change can currently be approved by any single maintainer. |
| **Gaps** | • `.github/CODEOWNERS` is a single catch-all rather than a workflow-specific rule. A path-scoped entry for `.github/workflows/**` would flag these changes for a distinct (ideally two-person) review path rather than folding into the default approval. A workflow change can currently be approved by any single maintainer. |
---
#### T-S7: Tag confusion / replay
| | |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Attacker with write access force-pushes an existing tag to point at a malicious commit, or pushes `v1.99.99` so that a release is published out of band. |
| **Likelihood** | Low (requires write access - assumed compromised at that point) |
| **Impact** | High |
| | |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Description** | Attacker with write access force-pushes an existing tag to point at a malicious commit, or pushes `v1.99.99` so that a release is published out of band. |
| **Likelihood** | Low (requires write access - assumed compromised at that point) |
| **Impact** | High |
| **Mitigations** | • npm rejects re-publishing an existing version - re-tagging you cannot overwrite the published `1.15.0`. <br>• Provenance attestation records the commit SHA the tag pointed to _at publish time_ - forensically verifiable. <br>• GitHub tag protection rules can prevent tag deletion/force-push. |
| **Gaps** | A _new_ malicious version (`v1.x.x`) is still publishable by anyone with tag-push rights - this collapses back into T-S3 (account security). |
| **Gaps** | A _new_ malicious version (`v1.x.x`) is still publishable by anyone with tag-push rights - this collapses back into T-S3 (account security). |
---
@@ -424,16 +448,16 @@ This is the model that protects **what gets published as `axios` on npm**. A suc
### 3.6 Summary: Project Risk Posture
| Threat | Likelihood | Impact | Current Posture | Priority Gap |
| ---------------------------- | ---------- | ------------ | --------------- | --------------------------------------------------------------- |
| T-S1 Malicious PR | High | Critical | 🟡 Adequate | Add CODEOWNERS for sensitive paths |
| **T-S2 Dev-dep steals keys** | **Medium** | **Critical** | **🔴 Weak** | **Local `--ignore-scripts`; no publish tokens on workstations** |
| **T-S3 Phishing** | **High** | **Critical** | **🟡 Partial** | **Mandate WebAuthn (not TOTP) for maintainer GitHub accounts** |
| T-S4 Runtime dep compromise | Low | Critical | 🟢 Good | - |
| T-S5 Build tampering | Low | Critical | 🟡 Adequate | Reproducible-build verification step |
| T-S6 Workflow tampering | Low | Critical | 🟢 Good | CODEOWNERS on `.github/` |
| T-S7 Tag replay | Low | High | 🟢 Good | - |
| T-S8 Typosquat | High | Medium | ⚪ Out of scope | - |
| Threat | Likelihood | Impact | Current Posture | Priority Gap |
| ---------------------------- | ---------- | ------------ | --------------- | --------------------------------------------------------------------- |
| T-S1 Malicious PR | High | Critical | 🟡 Adequate | Path-scoped CODEOWNERS for `lib/`, `.github/workflows/`, build config |
| **T-S2 Dev-dep steals keys** | **Medium** | **Critical** | **🔴 Weak** | **Local `--ignore-scripts`; no publish tokens on workstations** |
| **T-S3 Phishing** | **High** | **Critical** | 🟡 Partial | Mandate WebAuthn (not TOTP) for maintainer GitHub accounts |
| T-S4 Runtime dep compromise | Low | Critical | 🟢 Good | - |
| T-S5 Build tampering | Low | Critical | 🟡 Adequate | Reproducible-build verification step |
| T-S6 Workflow tampering | Low | Critical | 🟢 Good | Path-scoped CODEOWNERS entry for `.github/workflows/**` |
| T-S7 Tag replay | Low | High | 🟢 Good | - |
| T-S8 Typosquat | High | Medium | ⚪ Out of scope | - |
**The two threats most worth investing in are T-S2 and T-S3.** Both target the maintainer rather than the code, both bypass every in-repo control, and both have well-understood, low-cost mitigations that are currently a matter of individual maintainer discipline rather than enforced policy.