diff --git a/THREATMODEL.md b/THREATMODEL.md
index ae86bb7b..009dd4f9 100644
--- a/THREATMODEL.md
+++ b/THREATMODEL.md
@@ -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
`. |
-| **Likelihood** | Low |
-| **Impact** | Medium–High (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`. A related surface is **multipart per-part headers** — attacker-controlled `blob.type` or `blob.name` flowing into the multipart body. |
+| **Likelihood** | Low |
+| **Impact** | Medium–High (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.
• `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** | Low–Medium (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:
• `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).
• `lib/defaults/index.js` — `transformResponse` / `transformRequest` read `transitional`, `responseType`, `parseReviver`, `response` via an `own()` wrapper (fix for GHSA-3w6x-2g7m-8v23).
• `lib/adapters/http.js` — `transport`, `httpAgent`, `httpsAgent`, `lookup`, `family`, `http2Options`, etc. read via `hasOwnProp` (fix for GHSA-pf86-5x62-jrwf gadget set).
• `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).
• 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`).
• `maxBodyLength` bounds the request side.
• Both default to `-1` (unlimited). **Callers handling untrusted servers should set these.**
• 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).
• `maxBodyLength` bounds the request side, including when `maxRedirects === 0` (previously bypassed).
• 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.
• 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.
• `NO_PROXY` is honored (`lib/helpers/shouldBypassProxy.js`).
• 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.
• `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.
• 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.
• Exceeding the cap throws `AxiosError` with code `ERR_FORM_DATA_DEPTH_EXCEEDED` rather than crashing the process.
• 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.
• `pull_request` workflows run with no secrets and a read-only token - a malicious test cannot exfiltrate anything from CI.
• `pull_request_target` is **not** used (it would grant secrets to fork code).
• `zizmor` lints workflow files for known-dangerous patterns.
• 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.
• No automated diffing of `lib/` → `dist/` to catch build-output tampering.
• 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.
• `pull_request` workflows run with no secrets and a read-only token - a malicious test cannot exfiltrate anything from CI.
• `pull_request_target` is **not** used (it would grant secrets to fork code).
• `zizmor` lints workflow files for known-dangerous patterns.
• 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.
• No automated diffing of `lib/` → `dist/` to catch build-output tampering.
• `.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. ✅
• `permissions:` are minimal (`contents: read`, `id-token: write`).
• `persist-credentials: false` on checkout - the build steps cannot push back to the repo.
• `zizmor` lints workflows on every PR.
• 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`.
• Provenance attestation records the commit SHA the tag pointed to _at publish time_ - forensically verifiable.
• 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.