How Inconsistent IP Address Parsing Happens in JavaScript and How to Fix It
At a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-69192 |
| Package | ip-address < 10.3.1 |
| Severity | High |
| CWE | CWE-918 — Server-Side Request Forgery |
| Impact | SSRF, trust-boundary bypass |
| Fix | Upgrade to ip-address@10.3.1 |
Introduction
The core/http/react-ui frontend depends on @modelcontextprotocol/sdk, which in turn pulls in the ip-address npm package for IPv4/IPv6 address validation. On the surface, this looks like a routine utility dependency — but version 10.1.0 of ip-address contains a subtle, high-severity flaw in how its Address4 class handles octets with leading zeros.
When Address4 encounters an address like 010.0.0.1, it reads the leading-zero octet as decimal 10. But POSIX-compliant system resolvers — and many network stacks — interpret a leading zero as an octal prefix, making 010 equal to decimal 8. This one-digit difference is the entire attack surface.
If your application uses ip-address to validate or allowlist IPv4 addresses before passing them to an HTTP client or system resolver, an attacker can craft an address that passes your validation (because the library sees 10.0.0.1) but resolves differently at the network layer (because the OS sees 8.0.0.1 — or, more dangerously, routes to a private/loopback address entirely).
The Vulnerability Explained
The Octal Trap in IPv4 Notation
In C and many Unix-derived systems, numeric literals with a leading zero are octal. This convention leaked into early socket APIs and remains in POSIX inet_aton(). Consider these two interpretations of the same string:
Address string: 010.0.0.1
ip-address 10.1.0 (Address4): 10.0.0.1 ← decimal interpretation
POSIX inet_aton / many resolvers: 8.0.0.1 ← octal interpretation
Now imagine an attacker wants to reach 127.0.0.1 (loopback) on a server that blocks 127.x.x.x in its allowlist. They can try:
0177.0.0.1
ip-address 10.1.0sees:177.0.0.1— not loopback, passes the allowlist check ✅- System resolver sees:
0177= octal 127 →127.0.0.1— is loopback, connects to internal service ✅
The validation says "safe." The network says "internal." That gap is SSRF.
Where This Lives in the Dependency Tree
The vulnerable package entered the project through core/http/react-ui/bun.lock. Before the fix, the lock file resolved ip-address to version 10.1.0 as a transitive dependency of @modelcontextprotocol/sdk@1.27.1:
# bun.lock (before fix) — relevant excerpt
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", {
"dependencies": {
"@hono/node-server": "^1.19.9",
...
"hono": "^4.11.4",
...
}
}]
The ip-address package was not pinned with an override, so Bun resolved whatever version satisfied the semver range — which landed on the vulnerable 10.1.0.
Attack Scenario
Here's a concrete exploitation path for this application:
- The React UI communicates with a backend Go service (noted in the PR threat model as "a Go service — vulnerabilities in HTTP handlers are remotely exploitable").
- The frontend or its BFF (Backend for Frontend) uses
@modelcontextprotocol/sdkto validate or route MCP (Model Context Protocol) endpoint URLs. - An attacker supplies a crafted IP in a tool/resource URL:
http://0177.0.0.1:8080/admin Address4fromip-address 10.1.0parses0177as decimal177→177.0.0.1— not in the private range blocklist.- The SDK forwards the request. The OS resolver interprets
0177as octal → connects to127.0.0.1:8080/admin— the local admin interface. - The attacker has achieved SSRF to a loopback service that was never meant to be externally reachable.
The Fix
What Changed
The fix makes two targeted edits to enforce ip-address@10.3.1 across the entire dependency tree:
1. core/http/react-ui/bun.lock — Added an explicit override
"overrides": {
- "hono": "4.12.25",
+ "hono": "4.12.34",
+ "ip-address": "10.3.1",
},
The overrides field in Bun's lock file forces every package in the tree that depends on ip-address to resolve to exactly 10.3.1, regardless of what semver range they declare. Without this override, a transitive dependency could silently re-introduce the vulnerable version.
2. core/http/react-ui/package.json — Updated @modelcontextprotocol/sdk range
- "@modelcontextprotocol/sdk": "^1.25.1",
+ "@modelcontextprotocol/sdk": "^1.30.0",
Bumping to ^1.30.0 also picks up the SDK's own internal dependency updates, reducing the chance that the SDK itself re-pins to an older ip-address.
Why the Override Is the Critical Part
Simply upgrading the SDK might not be enough. If any other package in the tree declares "ip-address": "^10.0.0", Bun could still resolve 10.1.0 for that package. The "ip-address": "10.3.1" override entry acts as a fleet-wide pin — it is the authoritative, non-negotiable version for every consumer in this project.
What Version 10.3.1 Actually Fixes
ip-address 10.3.1 updates the Address4 parser to detect leading-zero octets and either:
- Reject them as invalid (strict mode), or
- Normalize them by stripping the leading zero before numeric conversion
Either behavior eliminates the decimal/octal discrepancy, ensuring that whatever IP string passes Address4 validation is also what the downstream resolver will connect to.
Prevention & Best Practices
1. Always Pin Transitive IP-Parsing Dependencies
IP address parsing is a security-sensitive operation. Any library that validates, normalizes, or routes based on IP addresses should be pinned to a known-good version using your package manager's override/resolution mechanism:
// package.json
"overrides": {
"ip-address": "10.3.1"
}
// For npm users, use "resolutions" (yarn) or "overrides" (npm 8.3+)
"resolutions": {
"ip-address": "10.3.1"
}
2. Test Leading-Zero Octets Explicitly
Add unit tests that assert your IP validation rejects or canonicalizes octal-style addresses:
import { Address4 } from 'ip-address';
// These should either throw or normalize — never silently pass as different IPs
const suspiciousAddresses = [
'0177.0.0.1', // octal 127 → loopback
'010.0.0.1', // octal 8
'0x7f.0.0.1', // hex 127 → loopback
];
for (const addr of suspiciousAddresses) {
try {
const parsed = new Address4(addr);
// Ensure the parsed address matches what a resolver would actually use
console.assert(parsed.address !== '127.0.0.1', `SSRF risk: ${addr}`);
} catch (e) {
// Rejection is also acceptable
}
}
3. Implement Defense-in-Depth for SSRF
IP parsing fixes are necessary but not sufficient. Layer your SSRF defenses:
- Allowlist only known-good IP ranges rather than blocklisting private ranges
- Use a dedicated SSRF-safe HTTP client that performs its own address validation after DNS resolution
- Validate at the network layer using egress firewall rules that block RFC-1918 and loopback ranges regardless of what the application layer allows
4. Run SCA Scanners in CI
This vulnerability was caught by Trivy's Software Composition Analysis (SCA) scanner, which matched the installed ip-address version against its CVE database. Integrate SCA into your CI pipeline so that vulnerable transitive dependencies are flagged before they reach production:
# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'HIGH,CRITICAL'
Relevant Standards
- OWASP SSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- CWE-918: Server-Side Request Forgery
- OWASP Top 10 — A10:2021: Server-Side Request Forgery
Key Takeaways
- Leading-zero octets are a parsing landmine:
0177.0.0.1is177.0.0.1toAddress4inip-address < 10.3.1but127.0.0.1(loopback) to the OS resolver — a gap wide enough to drive SSRF through. - Transitive dependencies need explicit overrides: The vulnerable
ip-addressversion entered through@modelcontextprotocol/sdk, not a direct dependency. The"ip-address": "10.3.1"override inbun.lockis what actually enforces the safe version fleet-wide. - Upgrading the direct dependency alone is not always enough: Bumping
@modelcontextprotocol/sdkto^1.30.0helps, but without theoverridesentry, other packages could still resolve the old version. - SSRF in MCP/BFF layers is high-impact: The Model Context Protocol SDK routes to external tool endpoints — if an attacker can influence those URLs, SSRF gives them access to any service reachable from the server's network, including internal admin APIs.
- SCA tooling catches what code review misses: No human reviewer scanning a
bun.lockdiff would spot a vulnerable transitive dependency version; automated scanners like Trivy are essential for this class of issue.
How Orbis AppSec Detected This
- Source: User-influenced IP address strings passed to
Address4via@modelcontextprotocol/sdkURL routing logic - Sink:
Address4constructor inip-address@10.1.0— the point where the octal/decimal discrepancy manifests before the address is forwarded to a network resolver - Missing control: No version override for
ip-addressinbun.lock, allowing the vulnerable10.1.0to be resolved as a transitive dependency; no normalization of leading-zero octets before network calls - CWE: CWE-918 — Server-Side Request Forgery (SSRF)
- Fix: Added
"ip-address": "10.3.1"to theoverridesblock inbun.lockand upgraded@modelcontextprotocol/sdkto^1.30.0inpackage.json, forcing the safe parser version across the entire dependency tree
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-69192 is a reminder that security vulnerabilities don't always look like buffer overflows or SQL injections — sometimes they hide in the gap between two components that both think they're parsing the same string correctly. The ip-address library's Address4 class and the system resolver were each internally consistent; the danger lived in their disagreement about what a leading zero means.
The fix is surgical: a two-line change to bun.lock and package.json that pins ip-address to 10.3.1 and ensures no future dependency resolution can silently downgrade it. But the broader lesson is about defense in depth — IP allowlist validation is only as strong as the parser implementing it, and parsers need to be held to the same standard as any other security control: pinned, tested, and monitored for CVEs.
For any application that routes requests based on user-supplied IP addresses or URLs — especially those integrating with protocol SDKs like MCP — this class of parser-differential vulnerability deserves explicit attention in your threat model.