How Octal/Decimal IP Parsing Ambiguity Happens in JavaScript and How to Fix It
Introduction
The plugins/sap-bw-query/mcp/ component handles MCP (Model Context Protocol) server logic for SAP BW Query integration — a context where outbound connections are made based on configuration and potentially user-influenced input. Buried in its dependency tree, ip-address@10.2.0 contained a subtle but dangerous flaw: its Address4 class decoded IPv4 octets with leading zeros as decimal, while every major OS resolver and many HTTP stacks decode them as octal. The result is that the string 010.0.0.1 means two completely different things depending on who is reading it — and attackers can exploit that gap to slip past IP-based access controls entirely.
This post walks through exactly what went wrong, how the exploit works, and what the upgrade to ip-address@10.3.1 actually fixes.
The Vulnerability Explained
A Tale of Two Parsers
IPv4 addresses like 010.0.0.1 look innocuous, but the leading zero carries a loaded meaning in C-style numeric literals: it signals octal notation. So 010 in octal is 8 in decimal, making 010.0.0.1 resolve to 8.0.0.1 at the OS level — not 10.0.0.1.
The vulnerable ip-address@10.2.0 library's Address4 class did not apply this octal rule. When application code called:
// ip-address 10.2.0 — VULNERABLE behavior
const { Address4 } = require('ip-address');
const addr = new Address4('010.168.1.1');
console.log(addr.toArray()); // [10, 168, 1, 1] ← decimal interpretation
The library returned 10.168.1.1. But when that same string was handed to Node's dns.lookup(), http.request(), or the underlying libc resolver, the OS parsed 010 as octal and connected to 8.168.1.1 instead.
The Dangerous Mismatch
Consider a typical SSRF-prevention pattern:
// Simplified allowlist check using ip-address 10.2.0
const { Address4 } = require('ip-address');
function isSafeDestination(ipString) {
const addr = new Address4(ipString);
const numeric = addr.bigInteger(); // computed from decimal-parsed octets
// Check: is this address in the public internet range?
return !isInternalRange(numeric); // passes for "010.168.1.1" → treats as 10.168.1.1
}
// Later, the application actually connects:
fetch(`http://${ipString}/api/data`); // OS resolves "010.168.1.1" → 8.168.1.1
The validation says "safe" because it computed 10.168.1.1. The actual TCP connection goes to 8.168.1.1. If 8.168.1.1 is an internal service (or a metadata endpoint like 169.254.169.254 via a crafted octal address), the attacker has achieved SSRF.
Constructing a Metadata-Service Bypass
AWS EC2's instance metadata service lives at 169.254.169.254. In octal, that address can be written as 0251.0376.0251.0376. An attacker submitting this string would find that:
ip-address@10.2.0parses each octet as decimal →251.376.251.376(invalid, gets rejected) — but with mixed leading-zero/non-leading-zero octets, more nuanced bypasses become possible.- For simpler cases like
0127.0.0.1(octal for87.0.0.1vs. loopback127.0.0.1), the library sees127.0.0.1(loopback — blocked), while the resolver sees87.0.0.1(public — allowed). The direction of the bypass depends on the allowlist logic, but the mismatch is always exploitable.
Real-World Impact for This Component
The SAP BW Query MCP plugin makes outbound calls as part of its query-routing logic. If any part of that pipeline validates a destination address using Address4 before passing it to a network call, an attacker who can influence the destination string can route requests to internal infrastructure — SAP BW backend servers, internal APIs, or cloud metadata endpoints — while the validation layer remains unaware.
The Fix
What Changed
The fix is a two-part change in the plugins/sap-bw-query/mcp/ directory:
1. package-lock.json — version pin updated
"node_modules/ip-address": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
- "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
+ "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
2. package.json — overrides field added
+ "overrides": {
+ "ip-address": "10.3.1"
+ }
The overrides field is critical. Without it, npm could still resolve a transitive dependency to the vulnerable 10.2.0 even if the direct dependency was updated. By declaring the override, the fix ensures that every copy of ip-address anywhere in the dependency tree is pinned to 10.3.1.
What 10.3.1 Actually Fixes
In ip-address@10.3.1, the Address4 parser was updated to treat leading-zero octets consistently with resolver behavior — either by rejecting them outright as ambiguous or by normalizing them to their octal values before any arithmetic. This means:
// ip-address 10.3.1 — FIXED behavior
const { Address4 } = require('ip-address');
// Ambiguous leading-zero input is now handled safely:
// Either throws an AddressError, or correctly interprets 010 as 8
const addr = new Address4('010.168.1.1');
// Result is now consistent with what the OS resolver will do
The library and the resolver now agree on what a given string means, eliminating the validation-bypass window entirely.
Prevention & Best Practices
1. Validate After Resolution, Not Before
The most robust SSRF defense resolves the hostname/IP first and then checks the resulting numeric address:
const dns = require('dns').promises;
async function isSafeDestination(host) {
const { address } = await dns.lookup(host); // get what the OS will actually connect to
const addr = new Address4(address); // parse the resolved canonical form
return !isInternalRange(addr.bigInteger()); // check the real destination
}
This approach is immune to parser-resolver mismatches because validation happens on the resolved address, not the raw input string.
2. Use npm overrides for Transitive Dependency Control
As demonstrated in this fix, package.json overrides (npm v8.3+) let you enforce a minimum version across the entire dependency tree:
{
"overrides": {
"ip-address": ">=10.3.1"
}
}
This is especially important for security fixes in widely-used utility libraries that appear as transitive dependencies.
3. Reject Non-Standard IP Formats at Input
Consider rejecting any IP address string that contains leading zeros before it ever reaches your parsing or networking code:
function rejectAmbiguousOctets(ipString) {
if (/\b0\d/.test(ipString)) {
throw new Error('Ambiguous leading-zero octet rejected');
}
}
4. Run Dependency Scanners in CI
Trivy, Snyk, and npm audit all flag known-vulnerable package versions. Integrate them into your CI pipeline so that vulnerabilities like this are caught before they reach production.
5. OWASP & CWE Guidance
- OWASP SSRF Prevention Cheat Sheet: always validate the resolved address, not the user-supplied string.
- CWE-918: Server-Side Request Forgery — the canonical classification for vulnerabilities where an attacker causes a server to make unintended network requests.
Key Takeaways
- Leading-zero IPv4 octets are a parser trap:
010means8to your OS but10toip-address@10.2.0— never assume two parsers agree on ambiguous input. - String-level IP allowlists are not enough: The mismatch in
Address4's decimal interpretation vs. resolver octal interpretation means a validated string can still reach a forbidden destination. - The
overridesfield inpackage.jsonis a security tool: Without it, transitive dependency trees can silently pull in the vulnerable10.2.0even after a direct-dependency upgrade. - SSRF defenses must happen post-resolution: Validate the IP address after DNS/resolver normalization, not on the raw user-supplied string.
- The SAP BW Query MCP plugin's outbound-connection context makes this high-priority: Any component that routes network requests based on configuration or user input is a prime SSRF target.
How Orbis AppSec Detected This
- Source: User-influenced or configuration-supplied IP address strings entering the SAP BW Query MCP plugin's connection-routing logic.
- Sink:
Address4constructor inip-address@10.2.0(resolved vianode_modules/ip-addressinplugins/sap-bw-query/mcp/package-lock.json) used to validate or parse destination addresses before outbound network calls. - Missing control: No normalization or rejection of leading-zero octets prior to parsing; no post-resolution re-validation to confirm the parsed and resolved addresses match.
- CWE: CWE-918 — Server-Side Request Forgery (SSRF).
- Fix: Upgraded
ip-addressfrom10.2.0to10.3.1and added anoverridesentry inpackage.jsonto enforce the fix across the full 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 sharp reminder that IP address validation is harder than it looks. A string like 010.168.1.1 is not self-evidently dangerous, but it carries a hidden ambiguity that splits application-layer parsers and OS resolvers onto different interpretive paths — and attackers can walk right through that gap. The fix in the SAP BW Query MCP plugin is surgical: upgrading ip-address to 10.3.1 and locking it with overrides ensures that the library and the resolver agree on every address, closing the SSRF window without touching any valid input paths.
For developers working with IP-based access controls in Node.js, the lesson is clear: never trust a pre-resolution string check. Resolve first, validate second, and keep your parsing libraries up to date.