Skip to content

Commit 47f0639

Browse files
Merge pull request #18 from hamdymohamedak/feat/secure-by-default
feat: secure-by-default errors, cache keys, and optional assertSafeUrl
2 parents 364ada0 + 3e48277 commit 47f0639

148 files changed

Lines changed: 702 additions & 580 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ node_modules/
77
.DS_Store
88
.env
99
.env.*
10+
.STEPS.md

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ For a feature matrix, examples, and execution order (middleware vs retry vs inte
2121

2222
---
2323

24+
## [Unreleased]
25+
26+
### Changed (security defaults)
27+
28+
- **`OpenFetchError.toShape()` / `toJSON()`** — Response **`data`** and **`headers`** are omitted unless you pass `includeResponseData: true` / `includeResponseHeaders: true`.
29+
- **`createCacheMiddleware`** — Cache keys **always** fold `authorization` and `cookie` unless `varyHeaderNames` is explicitly `[]`. Extra `varyHeaderNames` entries are merged with those two. The one-time `console.warn` now applies only when `varyHeaderNames: []` is explicit with auth/cookie and no custom `key`.
30+
31+
### Added
32+
33+
- **`assertSafeUrl`** on `OpenFetchConfig` — When true, runs `assertSafeHttpUrl` on the fully resolved URL before `fetch` (e.g. `createClient({ assertSafeUrl: true })`).
34+
2435
## [0.2.8] - 2026-04-14
2536

2637
### Added

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ createCacheMiddleware(store, {
111111
});
112112
```
113113

114-
Or build a custom `key` and use `appendCacheKeyVaryHeaders` from the package exports. See [SECURITY.md](SECURITY.md).
114+
Or build a custom `key` and use `appendCacheKeyVaryHeaders` from the package exports. See [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md).
115115

116116
### Retries and POST/PUT
117117

@@ -125,18 +125,19 @@ For low-level access without consuming the body in openFetch, set `rawResponse:
125125

126126
### Optional URL guard (server-side)
127127

128-
For URLs influenced by untrusted input, call `assertSafeHttpUrl(url)` before requesting. It blocks literal private/loopback IPs for `http:`/`https:`; it does not fix DNS rebinding — see [SECURITY.md](SECURITY.md).
128+
For URLs influenced by untrusted input, either call `assertSafeHttpUrl(url)` before requesting or enable **`assertSafeUrl: true`** on the client (defaults or per request). That blocks literal private/loopback IPs for `http:`/`https:` on the fully resolved URL; it does not fix DNS rebinding — see [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md).
129129

130130
### Errors and logging
131131

132-
`OpenFetchError.toShape()` omits `config.auth` and by default **redacts sensitive query parameters** in the `url` field; pass `redactSensitiveUrlQuery: false` only for trusted diagnostics. It may still include **response `data` and `headers`**. For client-facing or shared logs, use `toShape({ includeResponseData: false, includeResponseHeaders: false })`. The error instance itself can still hold full `config`; do not expose it raw.
132+
`OpenFetchError.toShape()` / `toJSON()` omit `config.auth` and, **by default**, omit response **`data`** and **`headers`**; pass `includeResponseData: true` / `includeResponseHeaders: true` when you need them for trusted diagnostics. By default the serialized `url` **redacts common sensitive query parameters**; pass `redactSensitiveUrlQuery: false` only in trusted environments. The error instance itself can still hold full `config`; do not expose it raw.
133133

134134
## Documentation
135135

136136
- **Guide (VitePress):** [openfetch-js.github.io/openfetch-docs/](https://openfetch-js.github.io/openfetch-docs/)
137-
- **Security:** [SECURITY.md](SECURITY.md)
138-
- **Claude Code:** add the marketplace with `claude plugin marketplace add openfetch-js/OpenFetch`, then `claude plugin install openfetch@openfetch-js` (see the plugin bundle in [`openfetchskill/`](openfetchskill/README.md)).
139-
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
137+
- **Security:** [SECURITY.md](https://github.com/openfetch-js/OpenFetch/blob/main/SECURITY.md)
138+
- **Claude Code:** `claude plugin marketplace add openfetch-js/OpenFetch`, then `claude plugin install openfetch@openfetch-js`. Published skill plugin: [openFetchSkill — README](https://github.com/openfetch-js/openFetchSkill/blob/main/README.md).
139+
- **Skill folder template (this monorepo):** [examples/claude-skill](https://github.com/openfetch-js/OpenFetch/tree/main/examples/claude-skill) — layout reference; see [examples/README.md](https://github.com/openfetch-js/OpenFetch/blob/main/examples/README.md).
140+
- **Contributing:** [CONTRIBUTING.md](https://github.com/openfetch-js/OpenFetch/blob/main/CONTRIBUTING.md)
140141

141142
## Requirements
142143

SECURITY.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ openfetch is a thin `fetch` wrapper. Callers supply URLs, headers, and bodies. T
77
- **Axios-class proxy CVEs (e.g. CVE-2025-62718 / `NO_PROXY` normalization)** — openfetch does **not** implement axios-style `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` matching. Outbound routing follows the host runtime’s `fetch` (and any platform proxy). Those CVEs therefore do **not** map to openfetch code paths; policy still belongs at the app, proxy, or mesh layer.
88

99
- **Network trust** — You choose endpoints. Blocking private IPs, metadata hosts, or open redirects is an **application** concern for partially trusted URLs.
10-
- **Secrets**`toShape()` on `OpenFetchError` avoids echoing `config.auth`, but the full `Error` object may still carry `config` (including credentials). Response bodies and headers in `toShape()` may still contain tokens or PII; use `toShape({ includeResponseData: false, includeResponseHeaders: false })` when serializing for untrusted clients or broad logs. By default, `toShape()` also **redacts common sensitive query parameters** in the serialized `url` (for example `token`, `code`, `password`); use `redactSensitiveUrlQuery: false` only for trusted diagnostics. The `debug()` plugin applies the same redaction to logged URLs. Never send raw errors to untrusted clients without redaction.
10+
- **Secrets**`toShape()` / `toJSON()` on `OpenFetchError` omit `config.auth` and, **by default**, omit response **`data`** and **`headers`** (pass `includeResponseData: true` / `includeResponseHeaders: true` only for trusted diagnostics). The live `Error` instance may still carry full `config` and `response`; never expose it raw to untrusted clients. By default, `toShape()` **redacts common sensitive query parameters** in the serialized `url` (for example `token`, `code`, `password`); use `redactSensitiveUrlQuery: false` only for trusted diagnostics. The `debug()` plugin applies the same redaction to logged URLs.
1111
- **Supply chain** — Install this package from npm or a verified Git tag; verify integrity with your package manager.
1212

1313
## Server-side usage and SSRF
@@ -17,24 +17,22 @@ When a URL (or part of it) comes from user input or another untrusted source on
1717
Mitigations (combine as appropriate):
1818

1919
- **Allowlist** hostnames or full URL prefixes your backend is allowed to call.
20-
- **Block literal private IPs**Use the optional helper `assertSafeHttpUrl(url)` before issuing the request. It rejects `http`/`https` URLs whose host is a loopback, private, link-local, or IPv4-mapped private address. On runtimes that use the WHATWG URL parser (including Node.js), hosts written as **decimal integers**, **hex/octal IPv4 segments**, or **shorthand** forms (for example `127.1`) are **normalized** to dotted-quad literals before `hostname` is read; `assertSafeHttpUrl` still applies its checks to that normalized host. It does **not** stop a public hostname from resolving to an internal IP (DNS rebinding); resolve and validate in a controlled resolver or use an outbound proxy.
20+
- **Block literal private IPs**Call `assertSafeHttpUrl(url)` before issuing the request, or set **`assertSafeUrl: true`** on `createClient` / per request so the fully resolved URL is checked automatically inside the dispatcher. The helper rejects `http`/`https` URLs whose host is a loopback, private, link-local, or IPv4-mapped private address. On runtimes that use the WHATWG URL parser (including Node.js), hosts written as **decimal integers**, **hex/octal IPv4 segments**, or **shorthand** forms (for example `127.1`) are **normalized** to dotted-quad literals before `hostname` is read; `assertSafeHttpUrl` still applies its checks to that normalized host. It does **not** stop a public hostname from resolving to an internal IP (DNS rebinding); resolve and validate in a controlled resolver or use an outbound proxy.
2121
- **Egress controls** — Route outbound HTTP through a proxy or service mesh that enforces policy.
2222

2323
## Memory cache and multi-tenant / authenticated traffic
2424

25-
`createCacheMiddleware` defaults to a cache key of ``METHOD fullUrl`` (plus optional custom `key`). That key does **not** include `Authorization`, `Cookie`, or other `Vary` inputs unless you add them.
25+
`createCacheMiddleware` builds a cache key from ``METHOD fullUrl`` (plus optional custom `key`). **By default**, `authorization` and `cookie` request header values are folded into the key (and any extra names you pass in `varyHeaderNames` are merged with those two), so authenticated GETs do not share entries across different credentials.
2626

27-
**Risk:** In a BFF, SSR, or shared worker, the first successful response for a URL can be served to **other** callers until the entry expires — cross-user or cross-tenant data leakage.
27+
**Risk:** If you set **`varyHeaderNames: []`** explicitly, the key is URL-only; the first successful response for that URL can be served to **other** callers until the entry expires.
2828

2929
**Mitigations:**
3030

31-
- Pass `varyHeaderNames: ["authorization", "cookie"]` (and any other headers your origin varies on), **or**
31+
- Omit `varyHeaderNames` (secure default), or pass additional header names (they are merged with `authorization` and `cookie`), **or**
3232
- Provide a custom `key` that incorporates a stable tenant or session identifier, **or**
3333
- Use `appendCacheKeyVaryHeaders` when building a custom key.
3434

35-
Unauthenticated, fully public GETs may keep the default key.
36-
37-
The middleware emits a **one-time `console.warn`** the first time it sees `Authorization` or `Cookie` on a cacheable request while `varyHeaderNames` is empty and no custom `key` is set. Suppress with `suppressAuthCacheKeyWarning: true` when you know the cache is safe (for example anonymous-only endpoints).
35+
The middleware emits a **one-time `console.warn`** the first time it sees `Authorization` or `Cookie` while `varyHeaderNames` was explicitly set to `[]` and no custom `key` is set. Suppress with `suppressAuthCacheKeyWarning: true` when that configuration is intentional (for example anonymous-only CDN).
3836

3937
## Retry and non-idempotent methods
4038

dist/core/cache.d.ts.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/core/client.d.ts.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/core/dispatch.d.ts.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/core/error.d.ts.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/core/interceptors.d.ts.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/core/middleware.d.ts.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)