A single invisible tab character lets a request slip past the bouncer's guest list
CVE-2026-18174 targets @fastify/forwarded versions prior to 3.0.2. The library parses the X-Forwarded-For header and returns an array of IP-address strings for upstream consumers such as @fastify/proxy-addr, which feeds request.ip inside every Fastify application. The bug: when the header contains multiple entries separated by commas, the parser strips spaces (0x20) around each address but ignores horizontal tabs (0x09), even though RFC 7230 §3.2.3 defines optional whitespace (OWS) as both SP and HTAB. An attacker who sends X-Forwarded-For: 9.9.9.9,\t1.2.3.4 gets back "\t1.2.3.4" — a string with a leading tab — which fails any downstream exact-string comparison against an allowlist, blocklist, or rate-limit key. The single-entry fast path already called .trim() and was never affected; only the multi-entry code path was vulnerable.
The vendor scored this MEDIUM 5.3 with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N — and that is directionally honest. The flaw is unauthenticated and network-reachable, but the impact ceiling is *Integrity-Low*: you can trick an IP comparison, not steal data or pop a shell. Real-world exploitation is further gated by reverse-proxy configuration: cloud load balancers (ALB, GCP LB, Cloudflare) and hardened nginx configs (proxy_set_header X-Forwarded-For $remote_addr) overwrite the header entirely, rendering the attack moot. noisgate concurs with the MEDIUM bucket but trims the score slightly to 4.8 to reflect these deployment-layer frictions.
4 steps from start to impact.
Craft X-Forwarded-For with embedded tab
\t, 0x09) before a chosen IP address inside the X-Forwarded-For header. For example: X-Forwarded-For: 9.9.9.9,\t10.0.0.1. No special tooling is required — curl -H can inject arbitrary header bytes. This is trivially reproducible.- Attacker can send HTTP requests to the application or its front-end proxy
- Cloud load balancers (AWS ALB, GCP LB, Azure App Gateway, Cloudflare) typically set X-Forwarded-For to the observed source IP, stripping attacker-supplied values entirely
- nginx configured with
proxy_set_header X-Forwarded-For $remote_addroverwrites the header, preventing injection
Header passes through proxy to Fastify
X-Forwarded-For header rather than replace it. The common nginx directive proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for does this — it preserves the attacker-controlled prefix and appends the real IP. The full header reaches Fastify intact.- Reverse proxy uses append-mode forwarding (e.g.,
$proxy_add_x_forwarded_for) - Fastify has
trustProxyenabled (string, array, number, or function)
- Security-hardened proxy configs already use overwrite mode per vendor best practices
- Applications that do not enable
trustProxyare unaffected — Fastify ignores the header
$proxy_add_x_forwarded_for usage; grep Fastify startup code for trustProxy: true@fastify/forwarded returns tab-prefixed IP
@fastify/forwarded parser splits the comma-separated header and trims only spaces, not tabs. It returns ["\t10.0.0.1", "9.9.9.9"]. This tainted string flows into @fastify/proxy-addr, which determines request.ip. If the trust function operates on the raw string without normalization, the tab-prefixed value survives into application logic.- @fastify/forwarded < 3.0.2 installed
- The single-entry fast path (one IP, no comma) already used
.trim()and was never affected - Trust functions that parse IPs via
net.isIP()or CIDR matching will reject the malformed string rather than silently pass it
npm ls @fastify/forwarded on the application to check the installed versionIP-based security control bypassed
request.ip via exact-string matching — e.g., if (request.ip === '10.0.0.1') for an allowlist, or stores it as a rate-limit key — the leading tab causes the comparison to fail. The attacker evades the blocklist, bypasses rate limiting, or circumvents an IP-scoped authorization check. Impact is limited to the specific control that relies on string equality.- Application uses exact-string comparison on the parsed IP for a security-relevant decision
- No secondary normalization (
.trim(),ip.parse()) is applied before comparison
- Well-written rate-limiting plugins (e.g.,
@fastify/rate-limit) typically normalize keys - IP-based ACLs in mature applications usually use CIDR or subnet matching, not string equality
- Impact ceiling is bypass of one control — not RCE, not data exfiltration
request.ip or the output of forwarded(req)The supporting signals.
| In-the-wild exploitation | None observed. Not listed on CISA KEV. No GreyNoise tags or mass-scanning activity detected as of 2026-07-30. Disclosed < 48 hours ago. |
|---|---|
| Proof of concept | The advisory itself describes the trivial payload: X-Forwarded-For: 9.9.9.9,\t1.2.3.4. No standalone exploit repo or weaponized tooling has been published. Reproduction requires only curl. |
| EPSS score | Not yet scored — CVE published 2026-07-29. Expect a low probability (likely < 5th percentile) given I:L impact ceiling and no C/A component. |
| KEV status | Not listed. No CISA deadline applies. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N — 5.3 MEDIUM. Network-reachable, no auth, but integrity-low only. No confidentiality or availability impact. |
| Affected versions | @fastify/forwarded < 3.0.2. All prior versions (0.1.2 through 3.0.1) are vulnerable on the multi-entry parsing path. |
| Fixed version | @fastify/forwarded 3.0.2 (released 2026-07-28). The fix adds HTAB stripping to the multi-entry parser, aligning it with the single-entry fast path that already used .trim(). |
| Transitive exposure | Fastify core (~8.9M weekly npm downloads) depends on @fastify/forwarded transitively via @fastify/proxy-addr. Every Fastify app with trustProxy enabled is in the dependency chain, but exploitability requires the append-mode proxy + exact-string-match code pattern. |
| Disclosure timeline | Advisory published 2026-07-28 (GHSA-2849-m2w7-xm8f). CVE-2026-18174 assigned 2026-07-29. Fix shipped same day as advisory. |
| Reporter | Credited to mcollina (Matteo Collina, Fastify lead maintainer) — self-reported by the project. |
noisgate verdict.
The single most decisive factor is the integrity-low impact ceiling: this bug lets an attacker evade an exact-string IP comparison, but it cannot yield code execution, data disclosure, or denial of service. Real-world blast radius is further narrowed because most production Fastify deployments sit behind cloud or hardened proxy infrastructure that overwrites the attacker-controlled header before it reaches the parser.
Why this verdict
- Impact ceiling is Integrity-Low only. The CVSS vector explicitly scores C:N/I:L/A:N — no path to code execution, data theft, or service disruption exists from this parsing gap alone. It can only weaken one secondary control (IP matching).
- Proxy-layer friction reduces reachable population. Cloud load balancers (ALB, GCP LB, Cloudflare) and hardened nginx/HAProxy configurations overwrite
X-Forwarded-Forrather than appending, which eliminates the attacker's ability to inject the malicious tab character. A significant fraction of production Fastify deployments operate behind these overwrite-mode proxies. - Exact-string comparison is a fragile code pattern. Exploiting the tab-prefix requires the downstream application to compare
request.ipvia===string equality against an allowlist or use it as a verbatim hash key. Mature rate-limiting libraries, CIDR-based ACLs, and IP-parsing routines normalize or reject malformed input, neutralizing the vector. - Role multiplier: @fastify/forwarded is a Node.js utility library consumed inside web application tiers. It is not a canonically high-value infrastructure component (not a hypervisor, domain controller, kernel-mode agent, certificate authority, or network edge appliance). In the worst plausible high-value role — a Fastify-based API gateway — successful exploitation bypasses a rate limiter or IP ACL (tenant-scope impact), but does not chain to fleet compromise, domain takeover, or supply-chain pivot. The blast-radius floor does not push above MEDIUM.
- No exploitation evidence or weaponized tooling. The CVE is < 48 hours old, no PoC repos exist, no KEV listing, and no mass-scanning has been observed. The trivial nature of the payload (a tab character) means a PoC is unnecessary — but neither has any campaign leveraged this class of bug in the Fastify ecosystem historically.
Why not higher?
To reach HIGH, this bug would need to chain into a meaningful security breach — credential theft, RCE, or privilege escalation. The integrity-low impact ceiling blocks that: you can evade an IP check, but that alone does not compromise a host or exfiltrate data. The affected component is not a canonically high-value role (not a DC, hypervisor, or perimeter appliance), so the blast-radius floor does not elevate the verdict. No active exploitation or KEV listing provides additional upward pressure.
Why not lower?
Despite real friction, the vulnerability IS unauthenticated, network-accessible, and zero-interaction (AV:N/AC:L/PR:N/UI:N). IP-based security controls — especially rate limiting — are a real defensive layer that this bug weakens. Fastify's transitive dependency chain means millions of weekly installs carry the vulnerable code, even if only a subset are exploitable. Dropping below MEDIUM would understate the breadth of the dependency footprint and the ease of crafting the malicious header.
What to do — in priority order.
- Switch reverse proxy to overwrite mode for X-Forwarded-For — Configure nginx with
proxy_set_header X-Forwarded-For $remote_addr(not$proxy_add_x_forwarded_for), or ensure your cloud LB sets the header from the observed source IP. This eliminates attacker control over the header entirely. Deploy as part of the noisgate 365-day remediation window since no mitigation SLA applies for MEDIUM. - Add an onRequest hook to reject or normalize tabs in XFF — In your Fastify app, register a hook that strips
\tcharacters from theX-Forwarded-Forheader before processing:fastify.addHook('onRequest', (req, reply, done) => { if (req.headers['x-forwarded-for']) req.headers['x-forwarded-for'] = req.headers['x-forwarded-for'].replace(/\t/g, ''); done(); }). This is the vendor-recommended workaround if you cannot immediately upgrade. - Upgrade @fastify/forwarded to 3.0.2 — The definitive fix. Run
npm update @fastify/forwardedor pin>=3.0.2in your lockfile. This is a patch-level semver bump with no breaking changes. Target completion within the noisgate 365-day remediation window. - Audit IP comparison logic for normalization — Grep your codebase for exact-string comparisons on
request.ip(e.g.,=== '10.0.0.1'). Replace with parsed IP or CIDR comparison usingnet.isIP()or a library likeip-address. This hardens you against this class of bug regardless of the upstream parser.
- WAF rules blocking tab characters in all headers — overly broad; HTAB is legal OWS in many HTTP headers per RFC 7230 and blocking it globally will break legitimate traffic (e.g., multi-line header folding in older clients).
- Disabling trustProxy — this does prevent the vulnerable code path, but it also breaks
request.ipfor all legitimate proxy-forwarded requests, defeating the purpose of running behind a load balancer. - Network-level IP blocklisting of attackers — the attack spoofs the *forwarded* IP, not the source IP; the attacker's real socket address is not the one being evaded, so blocking source IPs doesn't address the bypass.
Crowdsourced verification payload.
Run this on any host where a Node.js application with @fastify/forwarded is deployed. Execute as the application user or any user with read access to node_modules. Example: bash check_cve_2026_18174.sh /opt/myapp
#!/usr/bin/env bash
# check_cve_2026_18174.sh — Detect CVE-2026-18174 in @fastify/forwarded
# Usage: bash check_cve_2026_18174.sh <path-to-node-project>
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
PROJECT_DIR="${1:-.}"
PKG_NAME="@fastify/forwarded"
FIXED_VERSION="3.0.2"
if [ ! -d "$PROJECT_DIR/node_modules" ]; then
echo "UNKNOWN — node_modules not found in $PROJECT_DIR"
exit 2
fi
PKG_JSON="$PROJECT_DIR/node_modules/$PKG_NAME/package.json"
if [ ! -f "$PKG_JSON" ]; then
echo "UNKNOWN — $PKG_NAME not installed in $PROJECT_DIR"
exit 2
fi
INSTALLED=$(node -e "process.stdout.write(require('$PKG_JSON').version)" 2>/dev/null)
if [ -z "$INSTALLED" ]; then
echo "UNKNOWN — could not read version from $PKG_JSON"
exit 2
fi
# Compare versions using node semver logic
RESULT=$(node -e "
const v = '$INSTALLED'.split('.').map(Number);
const f = '$FIXED_VERSION'.split('.').map(Number);
const cmp = v[0] - f[0] || v[1] - f[1] || v[2] - f[2];
process.stdout.write(cmp >= 0 ? 'PATCHED' : 'VULNERABLE');
" 2>/dev/null)
if [ "$RESULT" = "PATCHED" ]; then
echo "PATCHED — $PKG_NAME $INSTALLED >= $FIXED_VERSION (CVE-2026-18174)"
exit 0
elif [ "$RESULT" = "VULNERABLE" ]; then
echo "VULNERABLE — $PKG_NAME $INSTALLED < $FIXED_VERSION (CVE-2026-18174)"
exit 1
else
echo "UNKNOWN — version comparison failed for $INSTALLED"
exit 2
fiIf you remember one thing.
npm ls @fastify/forwarded across your Node.js estate to identify affected applications, then audit their reverse-proxy configurations — if your proxies overwrite X-Forwarded-For (most cloud LBs do), you are not practically exploitable even without the patch. Since this is a MEDIUM finding with no mitigation SLA under the noisgate framework, go straight to the 365-day noisgate remediation SLA and schedule the @fastify/forwarded upgrade to 3.0.2 in your next regular dependency-update cycle. If you discover any application that *does* use append-mode XFF forwarding *and* exact-string IP comparisons for security decisions, prioritize that app's upgrade or deploy the onRequest hook workaround within the current sprint. No KEV listing or active exploitation exists — this is a backlog item, not an incident.Sources
- GHSA-2849-m2w7-xm8f — @fastify/forwarded advisory
- @fastify/forwarded v3.0.1→v3.0.2 diff (fix commit)
- Fastify GitHub — X-Forwarded-For spoofing issue #5865
- @fastify/forwarded — npm package page
- Snyk package health — @fastify/forwarded
- RFC 7230 §3.2.3 — OWS definition (SP / HTAB)
- CISA Known Exploited Vulnerabilities Catalog
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.