← Back to Feed CACHED · 2026-07-30 03:42:13 · CACHE_KEY CVE-2026-18174
CVE-2026-18174 · CWE-20 · Disclosed 2026-07-29

@fastify/forwarded resolves client addresses from the X-Forwarded-For header

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

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.

"Tab-char parsing gap in XFF lets attackers evade exact-string IP checks — not a crisis."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Craft X-Forwarded-For with embedded tab

The attacker inserts a horizontal tab character (\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.
Conditions required:
  • Attacker can send HTTP requests to the application or its front-end proxy
Where this breaks in practice:
  • 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_addr overwrites the header, preventing injection
STEP 02

Header passes through proxy to Fastify

The reverse proxy must be configured to *append* the client IP to an existing 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.
Conditions required:
  • Reverse proxy uses append-mode forwarding (e.g., $proxy_add_x_forwarded_for)
  • Fastify has trustProxy enabled (string, array, number, or function)
Where this breaks in practice:
  • Security-hardened proxy configs already use overwrite mode per vendor best practices
  • Applications that do not enable trustProxy are unaffected — Fastify ignores the header
Detection/coverage: Audit nginx/HAProxy/Envoy configs for $proxy_add_x_forwarded_for usage; grep Fastify startup code for trustProxy: true
STEP 03

@fastify/forwarded returns tab-prefixed IP

The vulnerable @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.
Conditions required:
  • @fastify/forwarded < 3.0.2 installed
Where this breaks in practice:
  • 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
Detection/coverage: Run npm ls @fastify/forwarded on the application to check the installed version
STEP 04

IP-based security control bypassed

If the application compares 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.
Conditions required:
  • Application uses exact-string comparison on the parsed IP for a security-relevant decision
  • No secondary normalization (.trim(), ip.parse()) is applied before comparison
Where this breaks in practice:
  • 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
Detection/coverage: Grep application code for string comparisons on request.ip or the output of forwarded(req)
03 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone 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 conceptThe 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 scoreNot 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 statusNot listed. No CISA deadline applies.
CVSS vectorCVSS: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 exposureFastify 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 timelineAdvisory published 2026-07-28 (GHSA-2849-m2w7-xm8f). CVE-2026-18174 assigned 2026-07-29. Fix shipped same day as advisory.
ReporterCredited to mcollina (Matteo Collina, Fastify lead maintainer) — self-reported by the project.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to MEDIUM (4.8/10)

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.

HIGH Vulnerability mechanics and fix correctness
MEDIUM Fraction of Fastify deployments using append-mode XFF proxies
LOW Downstream applications relying on exact-string IP matching for security decisions

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-For rather 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.ip via === 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.

05 · Compensating Control

What to do — in priority order.

  1. 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.
  2. Add an onRequest hook to reject or normalize tabs in XFF — In your Fastify app, register a hook that strips \t characters from the X-Forwarded-For header 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.
  3. Upgrade @fastify/forwarded to 3.0.2 — The definitive fix. Run npm update @fastify/forwarded or pin >=3.0.2 in your lockfile. This is a patch-level semver bump with no breaking changes. Target completion within the noisgate 365-day remediation window.
  4. 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 using net.isIP() or a library like ip-address. This hardens you against this class of bug regardless of the upstream parser.
What doesn't work
  • 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.ip for 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.
06 · Verification

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

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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
fi
07 · Bottom Line

If you remember one thing.

TL;DR
This is a parsing-consistency bug, not a crisis. Your Monday-morning action: run 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

  1. GHSA-2849-m2w7-xm8f — @fastify/forwarded advisory
  2. @fastify/forwarded v3.0.1→v3.0.2 diff (fix commit)
  3. Fastify GitHub — X-Forwarded-For spoofing issue #5865
  4. @fastify/forwarded — npm package page
  5. Snyk package health — @fastify/forwarded
  6. RFC 7230 §3.2.3 — OWS definition (SP / HTAB)
  7. CISA Known Exploited Vulnerabilities Catalog
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.