CVE-2026-90711 is a critical, configuration-dependent flaw in the Node.js proxy-addr package. Under specific IPv4-mapped IPv6 trust-subnet configurations, an unauthenticated client can make attacker-supplied X-Forwarded-For data appear trusted. Express applications that use req.ip or req.ips for authorization, rate limiting, geolocation, or audit evidence need both a dependency check and a proxy-configuration review.
The OpenJS Foundation published the CVE on September 15, 2026. GitHub reviewed and updated its advisory on October 5, assigning a CVSS 3.1 base score of 9.1. The affected range is proxy-addr 1.1.0 through 2.0.7; version 2.0.8 contains the fix. There is no need to invent a proof of concept against a public service: the vulnerable condition can be found safely by inspecting the dependency tree and trust-subnet syntax.
Why proxy trust is a security boundary
A reverse proxy usually receives the client connection and forwards the request to the application. Because the application’s socket peer is then the proxy, frameworks use headers such as X-Forwarded-For to recover the original client address. Those headers are only reliable when every trusted edge removes or overwrites client-supplied forwarding headers.
Express implements its trust proxy setting through proxy-addr. Starting with req.socket.remoteAddress, the package checks the address chain from right to left and stops at the first untrusted hop. Express documents that the setting must match the real reverse-proxy topology. It also warns that trust proxy=true is safe only when the final trusted proxy overwrites X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto.
The lesson fits a broader web-testing principle: forwarded identity is input from a trust boundary, not an authenticated fact. Our OWASP web application security guide provides the wider authorization and logging context, while the pentest methodology guide explains how to record scope, evidence, and remediation without turning a validation test into an outage.
How CVE-2026-90711 is triggered
The vulnerable path requires more than an affected package version. A trust rule must use an IPv4-mapped IPv6 subnet with a prefix that does not cover the 96-bit mapped-address marker. The advisory gives this unsafe example:
app.set('trust proxy', ['::ffff:10.0.0.0/8']);The author likely intends to trust the IPv4 network 10.0.0.0/8. In mapped notation, however, the IPv4 bits follow a 96-bit IPv6 prefix. The equivalent mapped subnet is ::ffff:10.0.0.0/104, calculated as 96 plus the IPv4 /8. The clearest configuration is usually plain IPv4 notation:
app.set('trust proxy', ['10.0.0.0/8']);In affected releases, the short mapped prefix can compile with all-zero leading bits and match every IPv4 address rather than the named private network. The GitHub advisory says an unauthenticated client may then be trusted at hop zero. As a result, proxyaddr(req, trust) and Express req.ip or req.ips can return a value supplied in X-Forwarded-For.
The maintainer’s patch canonicalizes mapped candidates and blocks cross-family matching unless the trusted IPv6 subnet is genuinely IPv4-mapped and its prefix covers the mapped marker. The release also adds regression tests for unsafe /8, correct /104, plain IPv4, native IPv6, and boundary cases.
What an attacker may bypass
The package does not create an access-control policy by itself. Impact depends on how the application uses the derived address. A spoofed value becomes serious when code treats client IP as a security credential or high-confidence detection signal.
- IP allowlists: administrative routes or internal APIs may accept a forged address.
- Rate limits: a client can rotate or choose header values to evade controls keyed to
req.ip. - Fraud and account defense: risk scoring, velocity checks, and location rules may receive false input.
- Audit logging: incident responders may follow a planted address while losing the socket peer that actually connected.
- Geographic policy: location-based restrictions may be applied to an attacker-chosen source.
The flaw does not mean every Express installation is remotely exploitable. Applications that do not enable proxy trust, do not use the affected syntax, or do not make security decisions from the derived address may have limited or no practical exposure. State the preconditions clearly in tickets and reports.
Inventory affected applications
Check both direct and transitive dependencies. A project may never list proxy-addr in package.json because Express brings it into the resolved dependency tree.
npm ls proxy-addr --all
npm explain proxy-addr
npm audit --omit=devFor repositories that use other package managers, inspect the matching lockfile and dependency command. Search application code, environment-generated configuration, Helm values, deployment templates, and platform settings for trust proxy, proxyaddr.compile, mapped IPv6 notation, and custom functions that return a trusted-hop decision.
rg -n "trust proxy|proxyaddr.compile|::ffff:" .
-g '!node_modules' -g '!dist' -g '!build'Do not stop at source code. A safe source default can be replaced by an environment variable, generated config map, feature flag, or infrastructure template during deployment.
Patch and harden the complete proxy path
1. Resolve proxy-addr 2.0.8 or later
Update the direct dependency or regenerate the lockfile through the project’s approved package-management workflow. Confirm the deployed artifact, not only the developer workstation:
npm update proxy-addr
npm ls proxy-addr --allIf policy requires a controlled direct pin, use a version range that resolves to 2.0.8 or later and review the lockfile diff. Test the application’s client-IP behavior before production rollout.
2. Rewrite ambiguous subnet rules
Prefer plain IPv4 CIDR for IPv4 networks. If mapped notation is required, add 96 to the intended IPv4 prefix. For example, 10.0.0.0/8 maps to ::ffff:10.0.0.0/104, while 192.168.0.0/16 maps to ::ffff:192.168.0.0/112. Review native IPv6 rules independently; a broad range such as ::/1 should never be accepted merely because it parses.
3. Make the edge authoritative
- Block direct access to the application listener from untrusted networks.
- Configure the final trusted proxy to remove or overwrite incoming forwarding headers.
- Trust explicit proxy addresses or subnets instead of all hops.
- Avoid numeric hop counts when traffic can reach the application through paths of different lengths.
- Keep
req.socket.remoteAddressin security logs alongside the normalized client address and raw forwarding chain.
IP-derived controls should supplement authentication, not replace it. For rate limiting and account-abuse investigations, the workflow in our Cloudflare account-abuse analysis guide shows why request identifiers and stateful account signals are stronger than one address field.
Detection and incident review
Patching prevents future misclassification, but it does not reconstruct earlier client identity. If an exposed application used an unsafe trust rule, treat historical req.ip values as potentially attacker-controlled during that period.
- Compare application client-IP values with load balancer, ingress, CDN, WAF, and socket-peer telemetry.
- Hunt for privileged or rate-limited actions where the application address is internal but the edge observed an external source.
- Review unusual changes in
X-Forwarded-Forchain length, private/public address order, or address family. - Check whether allowlisted routes were reached without the expected identity, session, mTLS certificate, or network path.
- Preserve raw logs before normalizing fields so an overwritten display value does not destroy the original evidence.
A mismatch is a lead, not proof of exploitation. Multi-CDN routes, service meshes, health checks, and IPv4-to-IPv6 translation can create legitimate differences. Correlate timestamps, request IDs, authenticated principals, and edge telemetry before declaring an incident.
Hands-on lab: audit a synthetic Express project
This offline lab scans a disposable project for an affected lockfile entry and unsafe mapped-prefix syntax. It does not run Express, send spoofed traffic, contact a target, or install the vulnerable package. Prerequisites are Python 3.9 or later and an empty working directory.
Create the synthetic files
mkdir proxy-trust-lab
cd proxy-trust-lab
cat > app.js <<'EOF'
const express = require('express');
const app = express();
app.set('trust proxy', ['::ffff:10.0.0.0/8']);
EOF
cat > package-lock.json <<'EOF'
{
"name": "proxy-trust-lab",
"lockfileVersion": 3,
"packages": {
"node_modules/proxy-addr": { "version": "2.0.7" }
}
}
EOFCreate the audit script
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
PATCHED = (2, 0, 8)
MAPPED = re.compile(r"::ffff:((?:d{1,3}.){3}d{1,3})/(d{1,3})", re.I)
BROAD_TRUE = re.compile(r"app.set(s*['"]trust proxy['"]s*,s*trues*)")
def version_tuple(value):
match = re.match(r"^(d+).(d+).(d+)", value or "")
return tuple(map(int, match.groups())) if match else None
def dependency_version(lock):
package = lock.get("packages", {}).get("node_modules/proxy-addr", {})
if package.get("version"):
return package["version"]
return lock.get("dependencies", {}).get("proxy-addr", {}).get("version")
def main(root):
root = Path(root)
findings = []
lock_path = root / "package-lock.json"
if lock_path.exists():
lock = json.loads(lock_path.read_text(encoding="utf-8"))
version = dependency_version(lock)
parsed = version_tuple(version)
if parsed and parsed < PATCHED:
findings.append(
f"CRITICAL dependency: proxy-addr {version} is affected; install 2.0.8 or later"
)
for path in sorted(root.rglob("*")):
if path.suffix not in {".js", ".cjs", ".mjs", ".ts"}:
continue
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
for match in MAPPED.finditer(line):
address, prefix_text = match.groups()
prefix = int(prefix_text)
if prefix < 96:
suggested = 96 + prefix if prefix <= 32 else 96
findings.append(
f"HIGH {path.relative_to(root)}:{number}: mapped IPv6 trust subnet "
f"{match.group(0)} uses a prefix below /96; use {address}/{prefix} "
f"or ::ffff:{address}/{suggested}"
)
if BROAD_TRUE.search(line):
findings.append(
f"WARN {path.relative_to(root)}:{number}: trust proxy=true requires the edge proxy "
"to overwrite all X-Forwarded-* headers"
)
for finding in findings:
print(finding)
print(f"Summary: {len(findings)} finding(s)")
return 1 if findings else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: audit_proxy_trust.py PROJECT_DIR", file=sys.stderr)
raise SystemExit(2)
raise SystemExit(main(sys.argv[1]))Save the script as audit_proxy_trust.py, then run it against the current directory:
python3 audit_proxy_trust.py .Expected output:
CRITICAL dependency: proxy-addr 2.0.7 is affected; install 2.0.8 or later
HIGH app.js:4: mapped IPv6 trust subnet ::ffff:10.0.0.0/8 uses a prefix below /96; use 10.0.0.0/8 or ::ffff:10.0.0.0/104
Summary: 2 finding(s)The script exits with status 1 when it finds a problem, which allows use as a review or CI gate. It is intentionally conservative and does not prove exploitability. Confirm the deployed topology, edge-header behavior, and application use of req.ip before assigning business impact.
Troubleshooting and cleanup
- No dependency finding: confirm the repository uses
package-lock.json; pnpm and Yarn need their own lockfile parser or native audit command. - No source finding: scan generated deployment values and configuration repositories in addition to application files.
- JSON error: validate the synthetic lockfile and make sure quotation marks were copied exactly.
cd ..
rm -rf proxy-trust-labValidation checklist
- The deployed dependency tree resolves
proxy-addr2.0.8 or later. - IPv4 networks use plain IPv4 CIDR or mathematically correct mapped prefixes.
- Only documented proxy addresses or subnets are trusted.
- The edge overwrites forwarding headers received from clients.
- Direct network access to the application listener is blocked.
- Tests cover every production traffic path, including failover and health-check routes.
- Security logs retain socket peer, normalized client IP, raw forwarding chain, request ID, and authenticated principal.
- IP data is not the sole credential for privileged access.
Conclusion
CVE-2026-90711 is a sharp example of why address normalization belongs inside the security model. A trust rule that looks like a familiar private subnet can cross address families and turn an untrusted header into application identity. Upgrade to proxy-addr 2.0.8 or later, rewrite ambiguous mapped subnets, make the final proxy authoritative for forwarding headers, and verify the behavior through every real network path.
Primary sources
- GitHub reviewed advisory GHSA-jqcg-44mw-7w3h / CVE-2026-90711, updated October 5, 2026.
- OpenJS Foundation CNA security advisory index, CVE published September 15, 2026.
- proxy-addr 2.0.8 release.
- Maintainer patch and regression tests.
- Express documentation: running behind proxies.
- NIST National Vulnerability Database entry.






