first commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# `forwarded` — `X-Forwarded-Host` trust enforcement
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements CIDR-based trust enforcement for the optional
|
||||
`X-Forwarded-Host` header. Edge proxies (CDN, load balancer, reverse
|
||||
proxy) rewrite the inbound `Host` to their internal hostname and stash
|
||||
the public host on `X-Forwarded-Host`. The resolver may honour that
|
||||
header, but ONLY when (a) the operator opted in via
|
||||
`trustForwardedHost: true` and (b) the upstream IP belongs to a
|
||||
trusted-proxy CIDR — otherwise a malicious client could spoof its
|
||||
tenant by sending `X-Forwarded-Host: victim.api.example.com` directly
|
||||
to the origin.
|
||||
|
||||
This module owns the parser (`parseCidr`, `parseIp`) and the matcher
|
||||
(`cidrMatches`, `isTrustedProxy`) used by the resolver hot-path.
|
||||
|
||||
## Features
|
||||
|
||||
- IPv4 and IPv6 support (including `::` shorthand and zone-id stripping).
|
||||
- Byte-array comparison with bit-aware prefix masking — no string
|
||||
manipulation in the hot-path.
|
||||
- Family-disjoint match — an IPv6 CIDR never matches an IPv4 address.
|
||||
- `parseCidr` returns `null` on malformed input so the caller (options
|
||||
parser) surfaces a stable `INVALID_OPTIONS` error rather than
|
||||
silently widening the trust set.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
isTrustedProxy,
|
||||
parseCidr,
|
||||
} from "@suaveplan/api-tenant-resolver-subdomain/forwarded";
|
||||
|
||||
const cidrs = ["10.0.0.0/8", "2001:db8::/32"]
|
||||
.map((c) => parseCidr(c)!)
|
||||
.filter((c) => c !== null);
|
||||
isTrustedProxy("10.4.7.2", cidrs); // → true
|
||||
isTrustedProxy("8.8.8.8", cidrs); // → false
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
cidrMatches,
|
||||
parseCidr,
|
||||
parseIp,
|
||||
} from "@suaveplan/api-tenant-resolver-subdomain/forwarded";
|
||||
|
||||
const cidr = parseCidr("172.16.0.0/12");
|
||||
const ip = parseIp("172.16.5.99");
|
||||
if (cidr !== null && ip !== null && cidrMatches(ip, cidr)) {
|
||||
// ...trusted...
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ParsedCidr` — `{ bytes: Uint8Array; prefix: number; family: 4 | 6 }`.
|
||||
- `parseCidr(input: string): ParsedCidr | null`.
|
||||
- `parseIp(addr: string): { bytes: Uint8Array; family: 4 | 6 } | null`.
|
||||
- `cidrMatches(ip, cidr): boolean`.
|
||||
- `isTrustedProxy(remoteAddress, cidrs): boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
IPv6 parsing handles the canonical `::` double-colon expansion, zone-id
|
||||
suffixes (`%eth0` is stripped — never meaningful for trust checks),
|
||||
and rejects > 8 groups or non-hex content. Prefix matching extracts
|
||||
the full-byte run with a byte-equality loop, then applies a
|
||||
high-bit mask to the trailing partial byte; `prefix === 0` is the
|
||||
"match everything in this family" case and is handled implicitly
|
||||
(loop runs zero times, tail short-circuit returns `true`).
|
||||
|
||||
The module is dependency-free — no `node:net`, no `ipaddr.js`. This
|
||||
keeps the package universal and the CIDR parsing observable in
|
||||
tests without runtime injection.
|
||||
Reference in New Issue
Block a user