mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
513 lines
92 KiB
Markdown
513 lines
92 KiB
Markdown
# Axios threat model
|
|
|
|
This document describes the threat model for axios: a library used at runtime by millions of applications, and an open-source project with a build pipeline, release infrastructure, and human maintainers.
|
|
|
|
It is for maintainers, security researchers, and downstream consumers doing supply chain due diligence. If you find a gap, open a security advisory rather than a public issue.
|
|
|
|
---
|
|
|
|
## 1. Scope and methodology
|
|
|
|
We model two distinct systems:
|
|
|
|
| System | What is being protected | Who attacks it |
|
|
| ------------------ | ------------------------------------------- | ----------------------------------------------------------------- |
|
|
| Runtime | Applications that `import axios` | Malicious servers, network attackers, malicious application input |
|
|
| Project / SDLC | The integrity of what gets published to npm | Supply-chain attackers, phishers, malicious contributors |
|
|
|
|
For each system, we list assets, trust boundaries, threat actors, and threats, rated by likelihood x impact. When mitigations exist in the codebase, we cite the file. When they do not, we say so.
|
|
|
|
The runtime model is general by design. axios is a transport library and cannot know what its callers consider sensitive. The project model is specific and actionable.
|
|
|
|
---
|
|
|
|
## 2. Runtime threat model
|
|
|
|
### 2.1 System overview
|
|
|
|
```
|
|
┌─────────────────┐
|
|
│ Application │ ← trusted: writes the config, owns the secrets
|
|
│ (caller code) │
|
|
└────────┬────────┘
|
|
│ axios(config)
|
|
┌────────▼────────┐
|
|
│ Interceptors │ ← caller-supplied code, runs in-process
|
|
├─────────────────┤
|
|
│ Config merge │ ← lib/core/mergeConfig.js
|
|
│ URL build │ ← lib/core/buildFullPath.js, lib/helpers/buildURL.js
|
|
│ Header build │ ← lib/core/AxiosHeaders.js
|
|
├─────────────────┤
|
|
│ Adapter │ ← http.js / xhr.js / fetch.js
|
|
└────────┬────────┘
|
|
│
|
|
═════════▼═════════ ← TRUST BOUNDARY (network)
|
|
│
|
|
┌────────▼────────┐
|
|
│ Proxy (opt.) │ ← partially trusted (sees plaintext if HTTP)
|
|
└────────┬────────┘
|
|
┌────────▼────────┐
|
|
│ Origin server │ ← UNTRUSTED in the general case
|
|
│ + redirects │
|
|
└─────────────────┘
|
|
```
|
|
|
|
### 2.2 Assets
|
|
|
|
| Asset | Why it matters |
|
|
| ------------------------------ | ------------------------------------------------------------ |
|
|
| Credentials in transit | `config.auth`, `Authorization` headers, cookies, XSRF tokens |
|
|
| Request/response bodies | May contain PII, business secrets |
|
|
| The caller's process integrity | Prototype pollution can lead to RCE in some downstream gadgets |
|
|
| The caller's internal network | SSRF can pivot through the host running axios |
|
|
| Availability | Decompression bombs, redirect loops, slow-loris responses |
|
|
|
|
### 2.3 Trust boundaries
|
|
|
|
1. Caller to axios. The caller is fully trusted. Anything the caller passes in `config` is assumed intentional. axios does not defend against a malicious caller; that is a non-goal.
|
|
2. axios to network. Everything past the socket is untrusted: response status, headers, body, redirect `Location`, proxy responses.
|
|
3. axios to environment variables. `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` are read by `proxy-from-env`. An attacker who controls the environment can redirect all traffic. This is treated as trusted because it has the same privilege as the process, but it is a relevant pivot in container-escape and CI scenarios.
|
|
4. Caller-supplied hooks to axios internals. Interceptors, `transformRequest`, `transformResponse`, `paramsSerializer`, `beforeRedirect`, and custom adapters run with full process privilege. axios does not sandbox them.
|
|
|
|
### 2.4 Threat actors
|
|
|
|
| Actor | Capability |
|
|
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
| Malicious server | Controls every byte of the response. Most common. |
|
|
| On-path network attacker | MITM. Mitigated by TLS unless the caller disabled validation. |
|
|
| Malicious redirect target | A trusted server redirects to an attacker. The attacker sees whatever axios forwards. |
|
|
| Application user | Controls part of the request (e.g. a URL path segment, a query param, a header value) via the calling application. |
|
|
|
|
### 2.5 Threats
|
|
|
|
> Severity = Likelihood x Impact, rated for a typical server-side deployment. Browser deployments inherit the browser's same-origin policy and are generally lower risk for SSRF and credential leakage.
|
|
|
|
---
|
|
|
|
#### T-R1: SSRF via caller-controlled URL
|
|
|
|
| | |
|
|
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Application interpolates user input into `config.url` or `config.baseURL`. Attacker supplies `http://169.254.169.254/`, `http://localhost:6379/`, `file://`, `gopher://`, etc. |
|
|
| **Likelihood** | **High.** This is the #1 real-world axios misuse pattern. |
|
|
| **Impact** | **High.** Cloud metadata theft, internal service access. |
|
|
| **In scope?** | **Partially.** axios cannot know which URLs the caller intends to allow. |
|
|
| **Mitigations** | • `allowAbsoluteUrls: false` prevents a relative `url` from overriding `baseURL` (`lib/core/buildFullPath.js`). Defaults to `true` for back-compat. <br>• The HTTP adapter only speaks `http:`/`https:`/`file:`/`data:` (Node) or `http:`/`https:`/`file:`/`blob:`/`url:`/`data:` (browser); exotic schemes like `gopher:` are rejected (`lib/platform/node/index.js`, `lib/platform/browser/index.js`). <br>• No built-in host allowlist. Callers must validate destinations themselves. |
|
|
| **Residual risk** | Substantial. This is documented as caller responsibility. |
|
|
|
|
---
|
|
|
|
#### T-R2: Credential leakage on cross-origin redirect
|
|
|
|
| | |
|
|
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Caller sets `Authorization: Bearer …` and requests `https://api.trusted.com/x`. Server responds `302 Location: https://evil.com/`. Does the bearer token go to evil.com? |
|
|
| **Likelihood** | Medium |
|
|
| **Impact** | High (full credential theft) |
|
|
| **Mitigations** | • Node adapter delegates to `follow-redirects@^1.16.0`, which strips `Authorization`, `Cookie`, and `Proxy-Authorization` on cross-host redirects and on HTTPS→HTTP downgrades. <br>• `sensitiveHeaders` lets callers list custom secret-bearing headers (for example `X-API-Key`) that axios strips on cross-origin redirects. <br>• `maxRedirects` defaults to 5; set to `0` to handle redirects manually. <br>• `beforeRedirect` callback allows custom inspection. <br>• Browser adapters (XHR/fetch) delegate to the browser, which applies its own cross-origin credential rules. |
|
|
| **Residual risk** | Low for standard credential headers and configured custom secret headers. We inherit `follow-redirects`' security posture - it is a critical transitive dependency and its CVEs are our CVEs. Callers must list any custom secret headers they want stripped. |
|
|
|
|
---
|
|
|
|
#### 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>`. A related surface is multipart per-part headers: attacker-controlled `blob.type` or `blob.name` flowing into the multipart body. |
|
|
| **Likelihood** | Low |
|
|
| **Impact** | Medium to 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. <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, write side (polluting response / merge into a target object)
|
|
|
|
| | |
|
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Server returns `{"__proto__": {"isAdmin": true}}`. If axios merged this into an object naively, every `{}` in the process would gain `.isAdmin`. |
|
|
| **Likelihood** | Low (requires a downstream gadget to be exploitable) |
|
|
| **Impact** | High (process-wide state corruption, sometimes RCE) |
|
|
| **Mitigations** | • `JSON.parse` itself does not pollute (it creates own-properties named `__proto__`, not prototype links). <br>• Internal merge paths filter dangerous keys: `lib/utils.js` and `lib/core/mergeConfig.js` filter `__proto__` / `constructor` / `prototype`; `lib/helpers/formDataToJSON.js` filters `__proto__`. <br>• These were added in response to past advisories. A regression here is a P0. |
|
|
| **Residual risk** | Low, but this is an area of active attacker interest. New merge helpers must go through the same filtering. |
|
|
|
|
---
|
|
|
|
#### 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 to 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: <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 dependency, not from the caller's own intent, and the above mitigations 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`), 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. |
|
|
|
|
---
|
|
|
|
#### T-R7: XSRF token sent cross-origin
|
|
|
|
| | |
|
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Browser deployment. `xsrfCookieName` is set; attacker tricks the app into requesting `https://evil.com` and the XSRF token cookie value is attached as a header. |
|
|
| **Likelihood** | Low |
|
|
| **Impact** | Medium |
|
|
| **Mitigations** | `lib/helpers/resolveConfig.js` only attaches the XSRF header when `isURLSameOrigin()` passes (or when `withXSRFToken` is explicitly forced). This was the fix for **CVE-2023-45857**. |
|
|
| **Residual risk** | Low. The same-origin check uses the WHATWG `URL` parser (`lib/helpers/isURLSameOrigin.js`), which is robust against parser-differential attacks. |
|
|
|
|
---
|
|
|
|
#### T-R8: Sensitive data in error objects
|
|
|
|
| | |
|
|
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Request fails. `AxiosError` includes `config`, which includes `config.auth`, `config.headers.Authorization`, `config.httpsAgent` (with embedded client cert/key). Caller logs the error, exposing secrets in logs. |
|
|
| **Likelihood** | **High** |
|
|
| **Impact** | Medium to High |
|
|
| **Mitigations** | `AxiosError.toJSON()` (`lib/core/AxiosError.js`) produces a reduced view, but the live error object still carries the full config by reference. |
|
|
| **Residual risk** | Medium. Callers using structured loggers that walk object graphs (Winston, Pino with serializers, Sentry) will capture credentials unless they configure redaction. This is a documented risk, not a vulnerability, but it is the most common way axios users leak secrets in practice. |
|
|
|
|
---
|
|
|
|
#### 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`), with recent hardening for CIDR ranges, IPv6 literals, and wildcard patterns to close parser-differential edge cases. <br>• HTTPS through any proxy uses CONNECT tunneling via `https-proxy-agent` so the origin's cert is validated end-to-end and the proxy sees only SNI, never the URL, headers, or body. `Proxy-Authorization` is sent on the CONNECT request only, never on the wrapped TLS-protected request. |
|
|
| **Residual risk** | Low for HTTPS. High for plain HTTP: the proxy sees and can modify everything. |
|
|
|
|
---
|
|
|
|
#### T-R10: Malicious interceptor / adapter
|
|
|
|
| | |
|
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| **Description** | Caller installs a third-party "axios plugin" from npm that registers an interceptor exfiltrating every `Authorization` header. |
|
|
| **Likelihood** | Low to Medium |
|
|
| **Impact** | High |
|
|
| **In scope?** | **No.** Interceptors are caller-supplied code running in the caller's process. axios provides the hook; vetting what goes into it is the caller's job. |
|
|
| **Residual risk** | Out of scope, but worth documenting: there is no meaningful difference between `axios.interceptors.request.use(evil)` and `require('evil')`. |
|
|
|
|
---
|
|
|
|
#### 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:
|
|
|
|
- Sandbox or validate caller-supplied functions (interceptors, transforms, adapters, serializers).
|
|
- 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 fully compromised caller process (e.g. attacker-controlled code running inside the caller). 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.
|
|
- Defend against monkey-patched JavaScript or Node.js runtime APIs (`Object.keys`, `http.request`, `ClientRequest.prototype.setHeader`, `fetch`, etc.). If attacker-controlled code is already running in the same process, it can observe or alter requests below axios and this is outside axios' security boundary.
|
|
|
|
---
|
|
|
|
## 3. Project / supply chain threat model
|
|
|
|
This model protects what gets published as `axios` on npm. A successful attack here compromises every downstream consumer at once. Given axios' install base, this is the higher-risk half of the document.
|
|
|
|
### 3.1 System overview
|
|
|
|
```
|
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
|
│ Maintainer's │ │ Contributor's │ │ GitHub.com │
|
|
│ workstation │ │ fork + PR │ │ (source of │
|
|
│ │ │ │ │ truth) │
|
|
│ ! npm token? │ │ untrusted code │ │ │
|
|
│ ! SSH keys │ │ │ │ │
|
|
│ ! GPG keys │ │ │ │ │
|
|
└────────┬─────────┘ └────────┬─────────┘ └────────▲─────────┘
|
|
│ │ │
|
|
│ git push │ PR │
|
|
└───────────────────────┴───────────────────────┘
|
|
│
|
|
tag push: v1.x.y
|
|
│
|
|
┌──────────────▼─────────────┐
|
|
│ GitHub Actions │
|
|
│ .github/workflows/ │
|
|
│ publish.yml │
|
|
│ │
|
|
│ • npm ci --ignore-scripts │
|
|
│ • npm run build │
|
|
│ • npm publish │
|
|
│ --provenance │
|
|
│ │
|
|
│ OIDC to npm (no token) │
|
|
└──────────────┬─────────────┘
|
|
│
|
|
═══════════════▼═══════════════
|
|
registry.npmjs.org
|
|
axios@1.x.y
|
|
+ provenance attestation
|
|
```
|
|
|
|
### 3.2 Assets
|
|
|
|
| Asset | Compromise means… |
|
|
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **The npm `axios` package name** | Attacker can publish malware as `axios@1.x.y+1`. Game over for the ecosystem. |
|
|
| **npm publish capability** | Whether via token, OIDC trust, or account takeover. |
|
|
| **GitHub `axios/axios` write access** | Attacker can push a tag, which triggers publish. Or modify `publish.yml` itself. |
|
|
| **Maintainer GitHub accounts** | Transitively grants the above. |
|
|
| **Maintainer workstation secrets** | SSH keys (GitHub push), `~/.npmrc` token if present (direct publish), GPG keys (signed commits), cloud creds (lateral movement). |
|
|
| **Build determinism** | If `dist/` doesn't match `lib/`, a backdoor can hide in the minified bundle. |
|
|
| **Runtime dependency integrity** | `follow-redirects`, `form-data`, `proxy-from-env`, `https-proxy-agent` ship inside every axios install. |
|
|
|
|
### 3.3 Trust boundaries
|
|
|
|
1. Contributor PRs to main branch. PRs from forks are untrusted. CI runs them, but `pull_request` workflows have no access to secrets and use a read-only `GITHUB_TOKEN`.
|
|
2. Main branch to release tag. Pushing to `v1.x` does not publish. Only pushing a `v1.*.*` tag does. Tag push requires write access.
|
|
3. GitHub Actions to npm. This boundary is crossed via OIDC (`id-token: write` to npm trusted publisher). The repo has no long-lived `NPM_TOKEN` secret.
|
|
4. Maintainer workstation to everything else. This is the softest boundary. A maintainer's laptop is a high-value, low-assurance environment. See §3.5.
|
|
|
|
### 3.4 Threat actors
|
|
|
|
| Actor | Capability | Motivation |
|
|
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
| Drive-by contributor | Open a PR. No secrets, no write. | Sneak a backdoor past review. |
|
|
| Compromised dependency | Attempt to run code on `npm install` via lifecycle scripts. Blocked on maintainer workstations (project `.npmrc`) and in CI (`--ignore-scripts` on every job). Residual execution path: plugin code under `npm run build` / `test` / `lint`. | Steal tokens, inject into build. |
|
|
| Phisher | Send convincing emails/DMs. No technical access. | Maintainer GitHub/npm credential theft. |
|
|
| Compromised maintainer account | Full write. Can push tags. Can edit workflows. | Direct publish of malware. |
|
|
| GitHub / npm insider or platform compromise | Out of scope. We trust the platforms. | - |
|
|
|
|
### 3.5 Threats
|
|
|
|
---
|
|
|
|
#### 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, or 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, so a malicious test cannot exfiltrate anything from CI. <br>• `pull_request_target` is not used because it would grant secrets to fork code. <br>• `zizmor` lints workflow files for known-dangerous patterns. <br>• Branch protection on `v1.x`. <br>• Package, lockfile, and GitHub Actions update PRs are maintainer/bot-only; outside-collaborator PRs for those updates are closed. <br>• Path-scoped `.github/CODEOWNERS` flags sensitive paths explicitly: runtime source (`/lib/`, `/index.*`), build/release infrastructure (`rollup.config.js`, `package.json`, `package-lock.json`, `.npmrc`), CI automation (`.github/workflows/`, `.github/dependabot.yml`, `CODEOWNERS` itself), and security-critical docs (`THREATMODEL.md`, `SECURITY.md`). Changes to these paths surface the scoped ownership rule in the PR review UI distinct from the catch-all. The audit trail shows that the PR touched a sensitive path. |
|
|
| **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/` to `dist/` to catch build-output tampering. <br>Single-maintainer constraint: with `@jasonsaayman` as sole owner on every scoped path, CODEOWNERS cannot enforce a second reviewer. Two-person review on sensitive paths remains unavailable until a co-maintainer is added. Path-scoping is pre-staged for that event. |
|
|
|
|
---
|
|
|
|
#### T-S2: Compromised dev dependency steals maintainer keys
|
|
|
|
> Historically the weakest link. The project-level `.npmrc` and hardware-backed maintainer keys materially improve it, but build-tool plugin execution (Rollup/Babel/Vitest/ESLint) is still the top residual investment area. `ignore-scripts` does not affect those tools, and they run whenever a maintainer builds or tests.
|
|
|
|
| | |
|
|
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | One of the ~45 direct dev dependencies, or one of their thousands of transitive dependencies, is compromised (maintainer account takeover, expired domain re-registration, the usual). It ships a `postinstall` script that reads `~/.npmrc`, `~/.ssh/id_*`, `~/.config/gh/hosts.yml`, `~/.aws/credentials`, `~/.gnupg/` and POSTs them to an attacker. <br><br>The next time a maintainer runs `npm install` on their workstation, the script runs as the maintainer's user, with full filesystem access. No exploit needed. This is npm working as designed. |
|
|
| **Likelihood** | Medium and rising. This exact pattern has hit `event-stream`, `ua-parser-js`, `coa`, `rc`, `node-ipc`, `@solana/web3.js`, the Ledger connect-kit, the 2024 polyfill.io incident, and dozens more. axios' dev tree includes Babel, Rollup, Gulp, ESLint, Vitest, and Playwright, each pulling hundreds of transitives. The attack surface is enormous and refreshes on every `npm install`. |
|
|
| **Impact** | Critical. A stolen npm token with publish rights means direct malware publish. A stolen SSH key with GitHub push rights means tag push, then publish via CI. Either path ends the same way. |
|
|
| **Current mitigations** | • CI is protected: `publish.yml` runs `npm ci --ignore-scripts`, so a malicious lifecycle script cannot execute during the release build. <br>• CI uses OIDC, not a stored token. There is no `NPM_TOKEN` secret in GitHub for a malicious workflow step to steal. <br>• `package-lock.json` pins versions and integrity hashes. A new malicious version won't arrive silently, only on explicit update. <br>• Project-local `.npmrc` sets `ignore-scripts=true`, so `npm install` / `npm ci` in a contributor or maintainer checkout does not execute lifecycle scripts (`preinstall`, `install`, `postinstall`, `prepare`) from any direct or transitive dependency. <br>• `husky` is the only `prepare` hook axios itself declares, and only writes `.git/hooks/`. With `ignore-scripts=true` it must be run manually (`npm rebuild husky && npx husky`), documented in the README "Contributing / Local setup" section. |
|
|
| **Gaps: workstation** | `ignore-scripts=true` neutralizes the lifecycle-script path, but it does not neutralize build-time code execution. A malicious Rollup / Babel / Terser / ESLint / Vitest plugin still runs when a maintainer executes `npm run build` / `npm test` / `npm run lint`. Those are not lifecycle scripts; they are tools the maintainer explicitly invoked. <br><br>The lockfile pins which packages install, but if one of those pinned packages was already malicious when the lock was generated, or the maintainer runs `npm update` / `npm install <new-pkg>` without re-setting `ignore-scripts`, fresh lifecycle scripts can land. <br><br>The development environment still has full read access to every credential the maintainer's user can read once a build tool runs. Isolation (devcontainer / VM) remains the strongest control. |
|
|
|
|
Mitigations adopted and recommended. Adopted items are enforced via the repo; others depend on per-maintainer discipline.
|
|
|
|
1. Don't keep a publish-capable npm token on your workstation.
|
|
Publishing happens via GitHub Actions OIDC. There is no workflow that requires `npm publish` from a laptop. If `~/.npmrc` has a token, it should be read-only or scoped to unrelated packages. If there is nothing to steal, this attack path is defanged.
|
|
|
|
2. Run `npm install` / `npm ci` with `--ignore-scripts` locally. Adopted: project ships a `.npmrc` with `ignore-scripts=true`.
|
|
All `npm install` / `npm ci` runs in a contributor or maintainer checkout skip lifecycle scripts by default. To set up git hooks after install, run the one trusted script manually:
|
|
|
|
```
|
|
npm rebuild husky && npx husky
|
|
```
|
|
|
|
The minor inconvenience of manually running known-good post-install steps is the price of not running thousands of unknown ones. Contributors adding a new dev dependency must not override this flag.
|
|
|
|
3. Develop in an isolated environment.
|
|
A devcontainer, VM, or sandbox profile that does not have:
|
|
- `~/.ssh/` mounted (use a separate deploy key or SSH agent forwarding only when pushing)
|
|
- `~/.npmrc` with publish tokens
|
|
- `~/.config/gh/` with a `repo`-scoped GitHub token
|
|
- `~/.aws/`, `~/.config/gcloud/`, etc.
|
|
|
|
The dev environment should be able to read/write the repo working tree and reach the network for tests. Nothing else.
|
|
|
|
4. Use hardware-backed keys for GitHub. Adopted project-wide.
|
|
All maintainers use FIDO2/WebAuthn for GitHub auth and `sk-ssh-ed25519@openssh.com` for git push. A stolen `~/.ssh/id_ed25519_sk` is useless without the physical key. This converts "steal a file" into "steal a file and a physical object." Each maintainer should keep a backup key registered and stored separately.
|
|
|
|
5. Audit lockfile diffs on dependency-update PRs as carefully as code.
|
|
A 4000-line `package-lock.json` diff hides a lot. Tooling: `npm diff`, `lockfile-lint`, Socket.dev's PR integration. Pay particular attention to new packages with install scripts (`hasInstallScript: true` in the lockfile).
|
|
|
|
6. Don't add dev dependencies casually.
|
|
Each one is a recurring trust decision delegated to a stranger. Prefer tools that can run via `npx` on demand (not in `node_modules`) or that are already in the tree.
|
|
|
|
---
|
|
|
|
#### T-S3: Phishing to maintainer account takeover
|
|
|
|
| | |
|
|
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Maintainer receives a convincing email: <br>• "npm security alert: your axios package has been flagged, log in to verify ownership" links to a fake npm login; password + TOTP are captured and replayed in real time. <br>• "GitHub: @axios has been added to a new organization, review access" links to a fake GitHub OAuth consent screen; the attacker app gets `repo` scope. <br>• Social: a "recruiter" asks the maintainer to clone and `npm install` a "take-home assignment" repo. <br><br>npm and GitHub credentials for axios maintainers have been specifically targeted by these campaigns in the past. This is not theoretical. |
|
|
| **Likelihood** | High. These campaigns are continuous. |
|
|
| **Impact** | Critical. GitHub account to push tag to publish. npm account to publish directly. |
|
|
| **Mitigations** | • npm 2FA is required for publish on the `axios` package. <br>• OIDC publishing means there is no maintainer npm session involved in a normal release. This narrows the attack to GitHub. <br>• All maintainers authenticate to GitHub with hardware-backed WebAuthn/passkeys (FIDO2 security keys / platform authenticators). Origin-bound credentials cannot be relayed by a phishing proxy (Evilginx, Modlishka). TOTP alone is not permitted for maintainer accounts. <br>• Git push uses `sk-ssh-ed25519@openssh.com` hardware-resident SSH keys where supported. A stolen key file is useless without the physical device. |
|
|
| **Gaps** | • Enforcement is per-account policy, not verifiable from the repo itself. Onboarding/offboarding checklist should confirm hardware-key status. <br>• Incident-response runbook is documented in §3.7 and needs periodic rehearsal to stay useful. <br>• Each maintainer should register at least 2 hardware keys (primary + backup stored separately) to avoid lockout-driven fallback to weaker recovery methods. |
|
|
|
|
---
|
|
|
|
#### T-S4: Compromised runtime dependency
|
|
|
|
| | |
|
|
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| **Description** | `follow-redirects`, `form-data`, `proxy-from-env`, or `https-proxy-agent` ships a malicious version. Unlike T-S2, this code ends up in the published axios bundle / runtime rather than being limited to maintainer machines. Every axios consumer runs it. |
|
|
| **Likelihood** | Low (only 4 deps; all are mature, narrowly-scoped, and watched) |
|
|
| **Impact** | Critical |
|
|
| **Mitigations** | • Three runtime deps total, minimal by design. <br>• `^` ranges in `package.json` mean consumers may get newer patch versions than the lockfile pins. This is intentional, because consumers get security fixes, but it also means a malicious patch release of `follow-redirects` propagates without an axios release. <br>• `follow-redirects` is security-conscious and well-maintained; we track its advisories closely (multiple past axios releases were just `follow-redirects` bumps). <br>• Dependabot is configured (`.github/dependabot.yml`) for both npm and GitHub Actions, running weekly with grouped updates for production and development dependencies. The 7-day cooldown stays in place unless a critical vulnerability requires a maintainer-led manual update. |
|
|
| **Gaps** | • No vendoring/inlining considered. The deps are small enough that vendoring is plausible, but it would forfeit upstream security fixes. Current judgment: not worth it. |
|
|
|
|
---
|
|
|
|
#### T-S5: Build-output tampering (`dist/` != `lib/`)
|
|
|
|
| | |
|
|
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | The published tarball contains a `dist/axios.min.js` that does not match what `rollup` would produce from `lib/`. Nobody reads minified bundles. A backdoor here is invisible to source review. <br><br>Vectors: a malicious dev-dep Rollup/Babel/Terser plugin injects code at build time (T-S2 applied to CI), or a maintainer with a compromised workstation accidentally publishes a tampered local build. |
|
|
| **Likelihood** | Low |
|
|
| **Impact** | Critical |
|
|
| **Mitigations** | • Builds run only in CI as part of `publish.yml`, from a clean `npm ci --ignore-scripts` checkout. There is no "publish from laptop" path. <br>• `--ignore-scripts` means a malicious dev dependency cannot tamper with `node_modules` before the build, but it can still tamper during the build if it is a Rollup/Babel plugin. Those run as part of `npm run build`, not as lifecycle scripts. <br>• npm provenance (`--provenance`) cryptographically attests which workflow on which commit produced the tarball. Consumers can verify with `npm audit signatures`. This proves the build ran in GitHub Actions on a known SHA. It does not prove the build is correct, only that it is traceable. |
|
|
| **Gaps** | • The build is not currently reproducible in the strict sense. A third party cannot independently rebuild and get a byte-identical `dist/`. Timestamps, plugin ordering, and minifier nondeterminism would need to be locked down. <br>• `.github/workflows/verify-build-reproducibility.yml` performs a two-pass build-and-diff on PRs that touch build-related paths (`lib/**`, `rollup.config.js`, `package.json`, `package-lock.json`, and the workflow itself). It is currently non-blocking (`continue-on-error: true`). It surfaces divergence in the CI summary so reproducibility regressions are visible, without gating merges until the build is deterministic. Once divergence is eliminated, remove `continue-on-error` to promote this to a hard gate. |
|
|
|
|
---
|
|
|
|
#### T-S6: Workflow file tampering
|
|
|
|
| | |
|
|
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **Description** | Attacker with write access (or a merged PR that was not reviewed carefully) modifies `.github/workflows/publish.yml` to `curl` the OIDC token somewhere, or to add a step that patches `dist/` after the build. |
|
|
| **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, so the build steps cannot push back to the repo. <br>• `zizmor` lints workflows on every PR and push to `v1.x` (`.github/workflows/zizmor.yml`); results surface as GitHub code-scanning alerts via the `security-events: write` permission on that job. This job must remain in the required-checks set on `v1.x` branch protection for the mitigation to be binding. <br>• The `npm-publish` GitHub Environment can require designated reviewers before the job runs; a tampered workflow still pauses for human approval. <br>• CODEOWNERS carries a path-scoped rule for `/.github/workflows/` and `/.github/CODEOWNERS` itself, so workflow and ownership changes surface in the review UI as touching a scoped path rather than being folded into the default approval. |
|
|
| **Gaps** | • Single-maintainer constraint (see T-S1): with one owner, the path-scoped rule cannot enforce a second reviewer on workflow changes. The rule surfaces the sensitivity but does not block single-maintainer approval. Closing this requires adding a co-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 |
|
|
| **Mitigations** | • npm rejects re-publishing an existing version. Re-tagging cannot overwrite the published `1.15.0`. <br>• Provenance attestation records the commit SHA the tag pointed to at publish time, which is forensically verifiable. Consumers can confirm with `npm audit signatures axios` (documented in SECURITY.md). <br>• Tag protection rules: repository setting must forbid tag deletion and force-push for the `v1.*.*` pattern. This is a GitHub UI setting (Settings > Tags > rulesets), not file-based; enforcement is auditable via the Rulesets REST API. |
|
|
| **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). |
|
|
|
|
---
|
|
|
|
#### T-S8: Typosquatting / dependency confusion
|
|
|
|
| | |
|
|
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| **Description** | Attacker publishes `axois`, `axios-http`, `@axios/core`, etc., and waits for typos. Or publishes a package shadowing an internal name used in a consumer's monorepo. |
|
|
| **Likelihood** | High (these packages already exist) |
|
|
| **Impact** | Medium. Affects confused consumers, not axios itself |
|
|
| **In scope?** | Mostly out of scope; the axios project cannot police the npm namespace. |
|
|
| **Mitigations** | • npm has typosquat detection at publish time (imperfect). <br>• The `@axios/` npm scope is not owned by the project. `@axios/anything` can be registered by anyone. This is a gap, not a mitigation. <br>• Provenance gives consumers a way to verify they got the real thing. |
|
|
|
|
---
|
|
|
|
### 3.6 Summary: project risk posture
|
|
|
|
| Threat | Likelihood | Impact | Current Posture | Priority Gap |
|
|
| ---------------------------- | ---------- | ------------ | --------------- | --------------------------------------------------------------------- |
|
|
| T-S1 Malicious PR | High | Critical | Good | Second maintainer to enable two-person review on scoped paths |
|
|
| T-S2 Dev-dep steals keys | Medium | Critical | Partial | Isolated dev environment (devcontainer/VM); no publish tokens on workstations. Lifecycle scripts now blocked via project `.npmrc`, but build-tool plugins still execute |
|
|
| T-S3 Phishing | High | Critical | Good | Document phish-response runbook; require registered backup hardware key |
|
|
| T-S4 Runtime dep compromise | Low | Critical | Good | - |
|
|
| T-S5 Build tampering | Low | Critical | Adequate | Eliminate build non-determinism, then promote reproducibility check to blocking |
|
|
| T-S6 Workflow tampering | Low | Critical | Good | Second maintainer (two-person review) for `/.github/workflows/` |
|
|
| T-S7 Tag replay | Low | High | Good | - |
|
|
| T-S8 Typosquat | High | Medium | Out of scope | - |
|
|
|
|
The top remaining investment is T-S2 (dev-dependency compromise of maintainer workstations). Lifecycle-script execution is now blocked by the project-level `.npmrc`, and T-S3 phishing risk dropped materially once all maintainers moved to hardware-backed WebAuthn. Real-time credential relay no longer works. The residual T-S2 gap is build-tool plugin execution (Rollup/Babel/Vitest/ESLint), which `ignore-scripts` does not cover. Closing it requires running builds in an isolated environment without access to long-lived credentials.
|
|
|
|
---
|
|
|
|
### 3.7 Incident response runbook
|
|
|
|
If a maintainer suspects credential compromise (phish clicked, lost hardware key, unexpected tag/publish, leaked token in logs), execute the steps below in order. Speed matters more than completeness. A published malicious version affects every downstream consumer.
|
|
|
|
#### 1. Contain, minutes 0 to 15
|
|
|
|
- GitHub: revoke all active sessions (`https://github.com/settings/sessions`), revoke all OAuth/PAT tokens (`/settings/tokens`, `/settings/applications`), review authorized SSH keys and remove any unrecognised. If a PAT with `repo` or `admin:org` scope existed, assume leak.
|
|
- npm: run `npm token list` and `npm token revoke <token>` for any publish-capable token. If no CLI access, revoke via `https://www.npmjs.com/settings/<user>/tokens`. Rotate npm password and force sign-out of all sessions.
|
|
- Workstation: if a build/install ran malicious code, assume full-user compromise of the laptop. Unplug from trusted networks. Do not rely on AV; move to a clean machine for rotation steps.
|
|
|
|
#### 2. Assess, minutes 15 to 60
|
|
|
|
- Check `https://github.com/axios/axios/settings/security-log` and `https://github.com/<maintainer>/security/log` for unrecognised events (key adds, org changes, force-pushes, new tags).
|
|
- Verify recent tags match intent: `git log --tags --oneline -n 20`. Compare with `https://www.npmjs.com/package/axios?activeTab=versions`.
|
|
- For each recent publish, verify provenance: `npm audit signatures axios@<version>` and cross-check the `sourceCommit` in the provenance attestation against the tag's SHA. Divergence = investigate.
|
|
- Review `~/.npmrc`, `~/.ssh/`, `~/.config/gh/hosts.yml`, `~/.gnupg/` for tampering and unexpected files.
|
|
|
|
#### 3. Rotate, hour 1 to 4
|
|
|
|
- Generate new SSH keys on clean hardware. Remove old keys from GitHub. If using `sk-ssh-ed25519@openssh.com`, register new hardware key first, then deregister the old one. Do not leave the account with zero registered keys.
|
|
- Re-enrol WebAuthn authenticators (both primary and backup). Deregister lost/compromised authenticators.
|
|
- Rotate GPG keys if signed commits are used; upload new key to GitHub.
|
|
- Rotate any cloud credentials (`~/.aws/`, `~/.config/gcloud/`) and any tokens present on the compromised machine.
|
|
|
|
#### 4. Notify, hour 1 onward
|
|
|
|
- npm security: `security@npmjs.com`. Include package name, suspected versions, timeline.
|
|
- GitHub security: `https://github.com/contact`, Security category. Request an investigation of the account.
|
|
- Downstream: open a GitHub security advisory (`https://github.com/axios/axios/security/advisories/new`) as soon as a malicious version is confirmed published. Do not wait for a fix. Users need to pin away from the bad version.
|
|
- Co-maintainers (when present): notify via out-of-band channel (phone/Signal), not through the compromised channel.
|
|
|
|
#### 5. Unpublish / deprecate, hour 1 to 24
|
|
|
|
- npm allows `npm unpublish <pkg>@<version>` within 72 hours of publish. After that, use `npm deprecate <pkg>@<version> "<reason>"` with a message pointing to the advisory.
|
|
- Publish a patched version that bumps semver above the malicious one, so `^` consumers move forward automatically.
|
|
|
|
#### 6. Post-mortem, within 1 week
|
|
|
|
- Write up timeline: initial vector, dwell time, scope, mitigations applied.
|
|
- Update this threat model if the incident reveals a gap not captured here.
|
|
- File a PR if any mitigation can be codified (new CI check, new lint rule, new CODEOWNERS path).
|
|
|
|
Keep this runbook current. A runbook no one has rehearsed is a document, not a control.
|
|
|
|
---
|
|
|
|
_This document describes intent and current understanding. It does not constitute a security guarantee. To report a gap in the model itself, use the same private advisory channel as for code vulnerabilities._
|