← Back to Feed CACHED · 2026-11-02 11:32:07 · CACHE_KEY CVE-2026-117518
CVE-2026-117518 · CWE-22 · Disclosed 2026-11-02

@fastify/reply-from backslash dot-segment path traversal in proxy target construction

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

A bouncer checks only forward slashes, but the turnstile quietly converts backslashes into slashes

The @fastify/reply-from plugin checks proxy paths for dot-segments using forward-slash separators, then builds the outgoing URL with the WHATWG URL parser. Under http, https, ws, and wss, that parser normalizes backslash characters and their percent-encoded form (%5c) into forward slashes. An attacker can therefore send %5c..%5c..%5cadmin or similar after a trusted prefix; the validation step sees an innocuous path, but the constructed upstream URL escapes the prefix and points at admin or another endpoint. Affected versions are up to and including 12.8.0; fixed in 12.8.1, which rejects any segment resolving to single or double dot plus control characters and ambiguous whitespace before URL construction. There is no workaround other than upgrading.

The vendor severity HIGH at 8.2 is fair for the worst plausible internet-facing API gateway/BFF use: unauthenticated remote, low complexity, and confidentiality impact can span many upstream endpoints. The real-world population is narrower, because this only matters where the application passes user-controlled path segments into the target URL behind a prefix or relies on an upstream route guard before forwarding; many Fastify apps use fixed rewrites or never expose the proxy publicly. The lack of KEV listing and no observed in-the-wild exploitation pull the operational ceiling below the pure CVSS math, but the plausible high-value use as an internal API gateway keeps this above MEDIUM. No live primary URL for this CVE was available for independent confirmation, so the assessment leans on the authoritative description supplied in the prompt.

"Backslash dot-segments slip through the prefix check and land on upstream endpoints the guard never intended."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a reachable Fastify app using prefix-scoped reply-from

Recon identifies an exposed Fastify application likely using @fastify/reply-from to proxy requests to an upstream service. The attacker verifies that path after a prefix is appended to the proxy target, such as /api/v1/... forwarding to http://internal:9000/api/v1/.... This is the prerequisite for the traversal to matter.
Conditions required:
  • Application uses @fastify/reply-from <=12.8.0
  • Proxy target is built from the request path after a trusted prefix or behind a route guard
  • Attacker can reach the proxy endpoint directly or via an internal request
Where this breaks in practice:
  • Many Fastify deployments use fixed path mappings and do not pass user-controlled path segments to reply-from
  • The proxy may be an internal service not reachable from the internet
  • Not all applications expose a prefix-scoped proxy path
Detection/coverage: SBOM or package-lock review finds @fastify/reply-from versions; dynamic scanners do not directly fingerprint this fork of prefix-scoped use.
STEP 02

Send backslash dot-segment payload past the forward-slash check

The attacker uses curl with --path-as-is or Burp Repeater to send GET /api/v1/%5c..%5c..%5cadmin HTTP/1.1. @fastify/reply-from validates the path only for forward-slash dot-segments, so the encoded backslashes pass. The WHATWG URL parser then normalizes each %5c or raw \ into /, producing http://internal:9000/api/v1/../../admin or similar.
Conditions required:
  • Target supports HTTP/HTTPS/WS/WSS schemes (WHATWG normalization applies)
  • Encoded/raw backslash reaches Node.js URL parser
  • Upstream path after prefix contains enough depth for .. segments
Where this breaks in practice:
  • Some WAFs or reverse proxies reject or normalize encoded backslashes before they reach Fastify
  • URL path parsing differences in ingress may strip or decode %5c inconsistently
  • The attacker needs a valid prefix with at least one traversable segment
Detection/coverage: WAF signatures for generic dot-segment traversal may not match backslash/encoded-backslash variants; specialized API gateways with strict path canonicalizers can flag it.
STEP 03

Upstream URL resolves outside the intended prefix

After normalization, reply-from constructs the upstream target with an escaped path. The upstream HTTP server receives a request for the unintended endpoint instead of the intended prefix-scoped resource. This can include admin routes, configuration endpoints, or internal APIs that were assumed unreachable through the proxy.
Conditions required:
  • The upstream server has a resource at the traversed path
  • The upstream does not independently re-check the normalized path
  • The HTTP method of the request is allowed for that endpoint
Where this breaks in practice:
  • Many upstream services use additional authentication, CSRF, or allow-list routing that blocks unknown paths
  • The app may reject paths containing %2e%2e before reply-from if a later guard normalizes after the check
  • Non-sensitive endpoints may be the only reachable targets
Detection/coverage: SIEM or access logs can show unusual %5c traversal attempts; upstream access logs may show normalized backslash paths.
STEP 04

Read unintended upstream data or trigger limited integrity changes

If the escaped endpoint returns data or accepts a state-changing method such as POST/PUT, the attacker can read sensitive responses or cause limited integrity impact. The CVSS confidentiality impact is high because multiple upstream endpoints are reachable; integrity is limited because only those reachable through the proxy and allowed by the method can be changed.
Conditions required:
  • The upstream endpoint contains sensitive data or permits state-changing methods
  • The proxy passes response back to the attacker
  • No independent authorization check on the upstream blocks the request
Where this breaks in practice:
  • Modern API gateways and service meshes often enforce mTLS or auth at upstream, which reduces direct exposure
  • If only GET is permitted and endpoints are read-only, integrity is nil
  • Sensitive data may be split across endpoints, requiring multiple requests and path guesses
Detection/coverage: API discovery tools, DLP, and unusual-response-size monitoring can catch data exfiltration; endpoint-specific logging can reveal unauthorized access.
03 · Compensating Control

1
HIGH 7.3→IGNORE 0.0
SEVERITY REDUCED
Upgrade @fastify/reply-from to 12.8.1 — This is the only complete fix; it normalizes paths and rejects dot segments, control characters, and ambiguous whitespace before URL construction. Deploy as the noisgate remediation SLA for HIGH — vendor patch within 180 days, but we recommend much sooner for internet-facing APIs. If exposure is confirmed, treat as CRITICAL-style emergency patch.
2
HIGH 7.3→LOW 2.0
SEVERITY REDUCED
Temporarily disable or restrict prefix-scoped forwarding — If upgrade cannot happen within the noisgate mitigation SLA of ≤30 days, disable the vulnerable rewrite or restrict the proxy path to an explicit allow-list of static values that cannot contain backslash or dot segments. This breaks the primary attack step by removing user-controlled path construction.
3
HIGH 7.3→MEDIUM 4.5
SEVERITY REDUCED
Put the proxy behind a strict API gateway/WAF that canonicalizes and rejects encoded backslashes and dot-segments — Deploy within the noisgate mitigation SLA of ≤30 days as a compensating control. A normalizer that rejects %5c, \, and %2e%2e before Fastify prevents the bypass at the edge. But custom normalizers can be bypassed, so this is containment, not a fix.
4
HIGH 7.3→LOW 2.5
SEVERITY REDUCED
Segment upstream and require independent authentication on upstream endpoints — Reduce the blast radius by ensuring upstream services do not trust the proxy as the sole authentication mechanism and are network-segmented. This is architecture work with a noisgate remediation SLA of ≤180 days; it means even a traversal escape finds only non-sensitive endpoints. It does not fix the parsing bug but lowers the confidentiality impact.
What doesn't work
  • Relying on route guards in front of reply-from without validating the normalized incoming path — the guard sees the raw path before URL normalization and will miss backslash dot-segments.
  • Disabling encoded slashes only at the HTTP server — WHATWG URL normalization still converts backslashes or %5c to slashes during URL construction.
  • Generic WAF path-traversal signatures for ../ and %2e%2e/ — the backslash and percent-encoded variants require explicit canonicalization rules.
04 · Intelligence Metadata

The supporting signals.

CVE IDCVE-2026-117518
In-the-wild statusNo known exploitation; not in CISA KEV as of disclosure date
PoC availabilityNo public PoC identified; a raw request is trivial to replicate manually once a reachable prefix-scoped proxy is found
EPSSNot scored or not yet published; application-specific exploitation likely keeps it low
KEV statusNot listed
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N
Affected versions@fastify/reply-from <=12.8.0
Fixed versions12.8.1; upgrade required, no workaround other than upgrade
Exposure / scanningNo Shodan/Censys/GreyNoise signature for this npm plugin; scanning must be package-level via SBOM
Disclosure date2026-11-02; reporter not disclosed in available record

Sources.

  1. NVD CVE-2026-117518
  2. CVE.org record
  3. @fastify/reply-from on npm
  4. GitHub repository fastify/fastify-reply-from
  5. GitHub Releases for fastify-reply-from
  6. WHATWG URL Standard
  7. OWASP Path Traversal
  8. CWE-22 Improper Limitation of a Pathname to a Restricted Directory
05 · The Call

Final Verdict
= UNCHANGED to HIGH (7.3/10)

Why this verdict

  • Role multiplier: if deployed as an internet-facing API gateway or BFF fronting internal services, this bug turns an unauthenticated request into a multi-endpoint data disclosure primitive; that high-value role keeps the floor at HIGH even though many instances are low-consequence. Includes @fastify/reply-from as an app-level edge component reaching internal admin/data APIs.
  • Friction-adjusted: vendor CVSS 8.2 assumes any unauthenticated client can reach the vulnerable path; actually it needs an application that forwards user-controlled path after a prefix or relies on a guard, which is common but not universal.
  • No KEV or observed exploitation: no known in-the-wild use and no public PoC as of disclosure date, so the score declines slightly below the CVSS arithmetic to 7.3.

Why not higher?

Not CRITICAL because the affected package is not a canonical security control or data tier; it is a plugin/library where high-value use depends on the application design. The impact is primarily confidentiality with limited integrity, and there is no known active exploitation or supply-chain pivot. An attacker still needs a reachable prefix-scoped proxy and a sensitive upstream endpoint; not a default condition on most Fastify apps.

Why not lower?

Not MEDIUM because the bug is remotely triggerable without authentication and with low complexity; the WHATWG backslash normalization is deterministic. When used as an API gateway/BFF in front of internal APIs, the blast radius includes arbitrary upstream endpoint access, mass data disclosure, and possible limited integrity changes. The affected package is widely deployed in the Fastify ecosystem and patch adoption may lag.

06 · Verification

Crowdsourced verification payload.

Run this on the project directory or CI build agent that has the application's package-lock.json or node_modules. Invoke with python3 check_reply_from.py /path/to/project (or pass the lockfile directly). No special privileges are required; it only reads package metadata. Outputs VULNERABLE, PATCHED, or UNKNOWN and exits 1/0/2 respectively.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
'''
Check whether @fastify/reply-from is <= 12.8.0.

Usage:
    python3 check_reply_from.py /path/to/project
    python3 check_reply_from.py /path/to/package-lock.json

Exit codes:
    0 -> PATCHED
    1 -> VULNERABLE
    2 -> UNKNOWN
'''
import json
import os
import re
import sys

VULNERABLE_MAX = (12, 8, 0)

def parse_version(ver):
    if not ver:
        return None
    ver = ver.strip().lstrip('v')
    ver = re.split('[+-]', ver)[0]
    nums = re.findall('[0-9]+', ver)
    if not nums:
        return None
    nums = nums[:3]
    while len(nums) < 3:
        nums.append('0')
    return tuple(int(n) for n in nums[:3])

def is_vulnerable(ver):
    parsed = parse_version(ver)
    if parsed is None:
        return None
    return parsed <= VULNERABLE_MAX

def check_package_lock(path):
    if not os.path.isfile(path):
        return None
    with open(path, 'r', encoding='utf-8', errors='ignore') as fh:
        data = json.load(fh)
    version = None
    pkgs = data.get('packages', {})
    if 'node_modules/@fastify/reply-from' in pkgs:
        version = pkgs['node_modules/@fastify/reply-from'].get('version')
    elif '@fastify/reply-from' in pkgs:
        version = pkgs['@fastify/reply-from'].get('version')
    deps = data.get('dependencies', {})
    if not version and '@fastify/reply-from' in deps:
        version = deps['@fastify/reply-from'].get('version')
    return version

def check_node_modules(pkg_path):
    if os.path.isfile(pkg_path):
        with open(pkg_path, 'r', encoding='utf-8', errors='ignore') as fh:
            data = json.load(fh)
        return data.get('version')
    return None

def find_reply_from_pkg(root):
    for dirpath, dirnames, filenames in os.walk(root):
        if os.path.basename(dirpath) == 'reply-from' and os.path.basename(os.path.dirname(dirpath)) == '@fastify':
            return os.path.join(dirpath, 'package.json')
    return None

def main():
    target = sys.argv[1] if len(sys.argv) > 1 else '.'
    version = None

    if os.path.isfile(target):
        if target.endswith('package-lock.json') or target.endswith('npm-shrinkwrap.json'):
            version = check_package_lock(target)
        elif target.endswith('package.json'):
            print('UNKNOWN: direct package.json is not sufficient for transitive @fastify/reply-from')
            sys.exit(2)
        else:
            version = check_node_modules(target)
    elif os.path.isdir(target):
        version = check_package_lock(os.path.join(target, 'package-lock.json'))
        if version is None:
            version = check_package_lock(os.path.join(target, 'npm-shrinkwrap.json'))
        if version is None:
            pkg = find_reply_from_pkg(target)
            if pkg:
                version = check_node_modules(pkg)
    if version is None:
        print('UNKNOWN: @fastify/reply-from not found in provided path')
        sys.exit(2)

    vuln = is_vulnerable(version)
    if vuln is True:
        print(f'VULNERABLE: @fastify/reply-from {version} is <= 12.8.0')
        sys.exit(1)
    elif vuln is False:
        print(f'PATCHED: @fastify/reply-from {version} is > 12.8.0')
        sys.exit(0)
    else:
        print(f'UNKNOWN: cannot parse version {version}')
        sys.exit(2)

if __name__ == '__main__':
    main()