Introduction
In a Node.js application's dependency tree, we discovered a high-severity SSRF vulnerability lurking in the ip-address package at version 10.2.0. The package-lock.json file locked this vulnerable version, which handles IP address parsing and validation—a critical security boundary for any application that makes outbound requests based on user input.
The vulnerability, tracked as CVE-2026-69192, exploits a subtle but dangerous inconsistency: when you write an IP address like 0177.0.0.1, the ip-address library's Address4 class interprets those leading zeros as decimal notation, seeing it as 177.0.0.1. But when your operating system's resolver processes that same address, it interprets the leading zero as octal notation—meaning 0177 becomes 127 in decimal. The result? Your validation says "safe external IP," but the actual request goes to 127.0.0.1—localhost.
This matters for any developer using IP validation to protect against SSRF attacks, which is essentially everyone building applications that fetch URLs or connect to user-specified addresses.
The Vulnerability Explained
How Octal IP Parsing Works (And Doesn't)
IP addresses have a lesser-known feature: octets with leading zeros can be interpreted as octal numbers. This is a POSIX standard behavior that most system resolvers follow:
0177.0.0.1 → Octal: 127.0.0.1 (localhost!)
0300.0.0.1 → Octal: 192.0.0.1
010.0.0.1 → Octal: 8.0.0.1
The vulnerable ip-address version 10.2.0 ignored this convention entirely. Its Address4 class parsed all octets as decimal, regardless of leading zeros:
// How ip-address 10.2.0 parsed addresses (simplified)
// Input: "0177.0.0.1"
// Library sees: 177.0.0.1 (decimal interpretation)
// System resolver sees: 127.0.0.1 (octal interpretation)
The Attack Scenario
Imagine your application has SSRF protection that blocks requests to internal IP ranges:
const { Address4 } = require('ip-address');
function isBlockedIP(ipString) {
const addr = new Address4(ipString);
// Block localhost, private ranges, etc.
if (addr.isInSubnet(new Address4('127.0.0.0/8'))) return true;
if (addr.isInSubnet(new Address4('10.0.0.0/8'))) return true;
if (addr.isInSubnet(new Address4('192.168.0.0/16'))) return true;
return false;
}
// Attacker submits: "0177.0.0.1"
isBlockedIP("0177.0.0.1"); // Returns FALSE (sees 177.0.0.1)
// But when the request is made...
fetch("http://0177.0.0.1/admin/secrets"); // Actually hits 127.0.0.1!
An attacker could use this to:
- Access internal admin panels on localhost
- Reach cloud metadata endpoints (like AWS's 169.254.169.254 via 0251.0376.0251.0376)
- Probe internal network services that should be unreachable
- Exfiltrate data from internal APIs
Real-World Impact
This application uses Firebase Admin SDK and Stripe—both of which handle sensitive data. If any component validates user-supplied URLs or IP addresses before making requests (common in webhook validation, proxy functionality, or URL preview features), this parsing inconsistency could allow attackers to bypass those protections and access internal services or sensitive endpoints.
The Fix
The fix implemented in this PR is elegant in its simplicity: upgrade the ip-address package from 10.2.0 to 10.3.1, where the maintainers corrected the octal parsing behavior.
What Changed in package.json
// Before
{
"dependencies": {
"firebase-admin": "^13.10.0",
"stripe": "^22.3.0",
"supercompress-proxy": "^0.5.17"
}
}
// After
{
"dependencies": {
"firebase-admin": "^13.10.0",
"stripe": "^22.3.0",
"supercompress-proxy": "^0.5.17"
},
"overrides": {
"ip-address": "10.3.1"
}
}
Why Use npm Overrides?
The ip-address package isn't a direct dependency—it's a transitive dependency somewhere in the dependency tree (likely through supercompress-proxy or another package). The overrides field in package.json forces npm to use version 10.3.1 regardless of what version the parent packages request.
What Changed in package-lock.json
// Before
"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=="
}
// After
"node_modules/ip-address": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
"integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="
}
How Version 10.3.1 Fixes the Issue
The patched version now correctly interprets leading-zero octets as octal, matching the behavior of system resolvers:
// ip-address 10.3.1 behavior
const { Address4 } = require('ip-address');
// Input: "0177.0.0.1"
// Now correctly parsed as: 127.0.0.1 (octal interpretation)
// Validation and resolution are now CONSISTENT
This means your SSRF blocklist will correctly identify 0177.0.0.1 as localhost and block the request before it's made.
Prevention & Best Practices
1. Defense in Depth for SSRF
Never rely solely on IP validation. Implement multiple layers:
// Layer 1: Validate the input IP/hostname
// Layer 2: Resolve the hostname and validate the resolved IP
// Layer 3: Use network-level controls (firewall rules, VPC configuration)
// Layer 4: Limit outbound connectivity from your application
2. Prefer Allowlists Over Blocklists
Instead of blocking known-bad IPs, consider allowing only known-good destinations:
const ALLOWED_HOSTS = ['api.stripe.com', 'api.github.com'];
function isAllowedDestination(hostname) {
return ALLOWED_HOSTS.includes(hostname);
}
3. Validate After DNS Resolution
Always validate the resolved IP address, not just the user-provided string:
const dns = require('dns').promises;
const { Address4 } = require('ip-address');
async function safeResolve(hostname) {
const addresses = await dns.resolve4(hostname);
for (const ip of addresses) {
const addr = new Address4(ip);
if (isPrivateOrReserved(addr)) {
throw new Error('Resolved to blocked IP range');
}
}
return addresses[0];
}
4. Keep Dependencies Updated
Use automated tools to monitor for vulnerable dependencies:
# npm audit for vulnerability scanning
npm audit
# Use tools like Trivy for comprehensive scanning
trivy fs --scanners vuln .
5. Use npm Overrides for Transitive Dependencies
When a vulnerability exists in a transitive dependency, npm overrides provide a clean solution:
{
"overrides": {
"vulnerable-package": "^fixed.version"
}
}
Key Takeaways
-
Octal IP notation is a real attack vector: The obscure
0177.0.0.1syntax can bypass naive IP validation in any language where the validation library and system resolver disagree on interpretation. -
Transitive dependencies carry risk: The
ip-addressvulnerability wasn't in direct dependencies but hidden in the dependency tree—npm overridesis the correct mechanism to force upgrades. -
SSRF protection requires consistency: Your validation logic must interpret IP addresses exactly as the component making the actual request will interpret them.
-
The
supercompress-proxydependency path (or similar) pulled in the vulnerableip-addressversion—always audit your full dependency tree, not just direct dependencies. -
Version 10.3.1 specifically addresses octal parsing: This isn't just a general security update; it's a targeted fix for the decimal-vs-octal interpretation mismatch.
How Orbis AppSec Detected This
- Source: User-controlled URL or IP address input flowing through the application's request handling
- Sink: The
ip-addresslibrary'sAddress4class used for IP validation before outbound requests - Missing control: Consistent octal-aware IP parsing that matches system resolver behavior
- CWE: CWE-918 (Server-Side Request Forgery)
- Fix: Upgraded
ip-addressfrom 10.2.0 to 10.3.1 via npm overrides to ensure consistent IP address interpretation
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 demonstrates how subtle parsing inconsistencies can completely undermine security controls. The ip-address library's decimal interpretation of leading-zero octets, while technically valid in isolation, created a dangerous mismatch with how operating systems actually resolve these addresses.
The fix was straightforward—a version upgrade via npm overrides—but the vulnerability itself highlights the importance of understanding the full data flow in your applications. When validating IP addresses for security purposes, ensure your validation library interprets addresses exactly as the downstream components will.
For Node.js developers: audit your dependencies for ip-address versions below 10.3.1, and consider implementing defense-in-depth SSRF protections that don't rely solely on pre-request IP validation.