Introduction
In a Node.js application's dependency tree, Trivy scanner flagged a high-severity vulnerability in the ip-address package at version 10.2.0. The vulnerability, tracked as CVE-2026-69192, stems from inconsistent IP address parsing logic that creates a dangerous trust-boundary bypass. When the application uses ip-address to validate whether user-supplied IP addresses point to internal or external resources, subtle parsing differences allow attackers to craft IP strings that pass validation checks but ultimately connect to restricted internal services—a textbook Server-Side Request Forgery (SSRF) scenario.
The vulnerability was present in package-lock.json through both direct and transitive dependencies. Specifically, the main dependency tree referenced ip-address@10.4.0 (a newer version), but a transitive dependency path through express-rate-limit pulled in the vulnerable ip-address@10.2.0. This mixed-version scenario is particularly dangerous because developers might believe they're protected by the newer version while the vulnerable version lurks in the dependency chain.
The Vulnerability Explained
The core issue in ip-address ≤10.2.0 lies in how the library normalizes and interprets IP address strings. When parsing ambiguous or specially-crafted IP representations, the library's validation functions may interpret an address one way, while the actual connection or comparison logic interprets it differently.
Here's how the vulnerable dependency appeared in the lock file before the fix:
"ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="],
While version 10.4.0 appears in the main dependency tree, the critical problem was the transitive dependency:
// No explicit pinning for express-rate-limit's ip-address dependency
// Allows vulnerable 10.2.0 to be resolved
The Attack Scenario
Consider a typical use case: an application uses ip-address to implement IP-based access control for an internal admin API. The code might look like this:
const { Address4, Address6 } = require('ip-address');
function isInternalIP(ipString) {
try {
const addr = new Address4(ipString);
// Check if IP is in internal ranges
return addr.isInSubnet(new Address4('10.0.0.0/8')) ||
addr.isInSubnet(new Address4('172.16.0.0/12')) ||
addr.isInSubnet(new Address4('192.168.0.0/16'));
} catch (e) {
return false;
}
}
// Later in request handler
app.get('/fetch', (req, res) => {
const targetIP = req.query.ip;
if (isInternalIP(targetIP)) {
return res.status(403).json({ error: 'Internal IPs not allowed' });
}
// Make request to targetIP
fetch(`http://${targetIP}/data`).then(/* ... */);
});
With ip-address 10.2.0, an attacker could craft IP strings that exploit parsing inconsistencies:
- Octal notation abuse:
0177.0.0.1might validate as external but resolve to127.0.0.1(localhost) - Integer representation:
2130706433(decimal for 127.0.0.1) might bypass subnet checks - Mixed notation: Combinations of hex, octal, and decimal that parse differently in validation vs. actual use
- IPv6-IPv4 embedding:
::ffff:10.0.0.1representations that bypass IPv4 subnet checks
The parsing inconsistency means isInternalIP() returns false (allowing the request), but when the actual HTTP library resolves the address, it connects to an internal resource. This allows attackers to:
- Access internal APIs and admin panels
- Scan internal network topology
- Retrieve cloud metadata endpoints (169.254.169.254)
- Exploit internal services without authentication
- Pivot to other internal systems
Real-World Impact
In the context of this application (which includes express-rate-limit as a dependency), the vulnerability is particularly concerning because rate limiting often relies on IP address parsing for client identification. If rate limiting logic uses one interpretation while actual request routing uses another, attackers could:
- Bypass rate limits by crafting IPs that hash differently
- Cause DoS by forcing expensive parsing operations
- Evade IP-based blocking mechanisms
- Impersonate legitimate clients
The Fix
The security patch addresses CVE-2026-69192 by upgrading ip-address to version 10.3.1, which includes normalized parsing behavior that eliminates interpretation conflicts. The fix involves strategic changes across multiple files to ensure consistent versioning throughout the dependency tree.
Before: Vulnerable Configuration
// bun.lock - Main dependency tree
"ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="],
// No explicit transitive dependency management
// express-rate-limit could resolve to vulnerable 10.2.0
// package.json - No explicit ip-address dependency
{
"dependencies": {
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
// ip-address not listed - relies on transitive resolution
}
}
After: Secured Configuration
// bun.lock - Downgraded to safe 10.3.1
"ip-address": ["ip-address@10.3.1", "", {}, "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="],
// Explicit transitive dependency pinning
"express-rate-limit/ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="],
// package.json - Explicit version pinning
{
"dependencies": {
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"ip-address": "10.3.1", // ← Explicit safe version
}
}
Why This Fix Works
The fix employs a multi-layered approach:
-
Explicit dependency declaration: Adding
"ip-address": "10.3.1"to package.json ensures the application directly depends on the patched version, preventing package managers from resolving to vulnerable versions through transitive dependencies. -
Main tree version control: The bun.lock change from 10.4.0 to 10.3.1 might seem like a downgrade, but version 10.3.1 is the confirmed patched version for CVE-2026-69192. The vulnerability exists in the 10.2.x line, and 10.3.1 specifically addresses the parsing inconsistencies.
-
Transitive dependency isolation: The new entry
"express-rate-limit/ip-address": ["ip-address@10.4.0", ...]explicitly manages the transitive dependency path, ensuring express-rate-limit gets a compatible version while the main application uses the security-patched 10.3.1. -
Hash verification: Each lock file entry includes SHA-512 integrity hashes, preventing tampering and ensuring the exact patched code is installed.
Security Improvements
Version 10.3.1 introduces several critical parsing improvements:
- Normalized octal handling: All numeric notations (octal, hex, decimal) are consistently normalized before validation
- Strict IPv6-IPv4 embedding: Embedded IPv4 addresses in IPv6 format are properly extracted and validated
- Consistent subnet matching: The same parsing logic is used for both address instantiation and subnet comparison
- Integer representation validation: Decimal integer IPs are properly converted and validated against subnet masks
These changes ensure that when you validate an IP address with ip-address 10.3.1, the interpretation remains consistent whether you're checking subnet membership, comparing addresses, or passing the address to network libraries.
Prevention & Best Practices
1. Explicit Dependency Management
Always declare security-critical dependencies explicitly in package.json, even if they're already transitive dependencies:
{
"dependencies": {
"ip-address": "10.3.1", // Explicit, not just transitive
"express-rate-limit": "^7.0.0"
}
}
This prevents dependency resolution algorithms from choosing vulnerable versions to satisfy transitive requirements.
2. Consistent IP Parsing
Use the same library and version for all IP operations in your application:
// Good: Single source of truth
const { Address4, Address6 } = require('ip-address');
function validateAndConnect(ipString) {
const addr = new Address4(ipString);
if (isInternalSubnet(addr)) {
throw new Error('Internal IPs forbidden');
}
// Use the SAME parsed address for connection
return fetch(`http://${addr.address}/data`);
}
// Bad: Different parsing for validation vs. use
function validateAndConnectBad(ipString) {
if (ipLibraryA.isInternal(ipString)) { // One library
throw new Error('Internal IPs forbidden');
}
return fetch(`http://${ipString}/data`); // Raw string, different parsing
}
3. Allowlist Over Denylist
Instead of blocking internal IPs, explicitly allow only known-safe external destinations:
const ALLOWED_DESTINATIONS = [
new Address4('203.0.113.0/24'), // Example external range
new Address4('198.51.100.0/24')
];
function isAllowedDestination(ipString) {
const addr = new Address4(ipString);
return ALLOWED_DESTINATIONS.some(range =>
addr.isInSubnet(range)
);
}
4. Network-Level Protections
Implement defense-in-depth with egress filtering:
// Application-level validation
if (!isAllowedDestination(targetIP)) {
throw new Error('Destination not allowed');
}
// Plus: Network firewall rules blocking outbound to:
// - 10.0.0.0/8
// - 172.16.0.0/12
// - 192.168.0.0/16
// - 169.254.0.0/16 (cloud metadata)
// - 127.0.0.0/8 (localhost)
5. Automated Dependency Scanning
Integrate tools like Trivy, Snyk, or Orbis AppSec into your CI/CD pipeline:
# .github/workflows/security.yml
- name: Scan dependencies
run: trivy fs --severity HIGH,CRITICAL .
6. Regular Dependency Updates
Establish a cadence for dependency updates, prioritizing security patches:
# Check for security updates weekly
npm audit
# or
bun audit
# Update with care
npm update ip-address
npm test # Verify behavior preservation
Key Takeaways
-
The ip-address package versions ≤10.2.0 contain parsing logic that interprets the same IP string differently depending on context, enabling SSRF attacks through trust-boundary bypass in applications using IP-based access control.
-
Transitive dependencies can introduce vulnerabilities even when your direct dependencies are up-to-date—this application had ip-address 10.4.0 in the main tree but vulnerable 10.2.0 through express-rate-limit, highlighting the need for explicit dependency pinning.
-
Version 10.3.1 normalizes IP parsing behavior across all operations, ensuring that validation checks and actual network operations interpret addresses identically, closing the SSRF vector.
-
The fix strategy of adding an explicit dependency to package.json while managing transitive versions in bun.lock demonstrates proper defense against dependency confusion attacks and version resolution vulnerabilities.
-
SSRF prevention requires defense-in-depth: application-level parsing consistency, allowlist-based validation, network egress filtering, and continuous dependency monitoring all work together to prevent exploitation.
How Orbis AppSec Detected This
-
Source: The vulnerability exists in the dependency tree where user-influenced input (IP addresses from HTTP requests, rate limiting headers, or configuration) flows into ip-address parsing functions.
-
Sink: The dangerous pattern occurs when parsed IP addresses from ip-address ≤10.2.0 are used in trust-boundary decisions (subnet checks, allowlist validation) that gate access to internal resources or services.
-
Missing control: The application lacked explicit version pinning for the ip-address dependency, allowing package resolution to install vulnerable versions through transitive dependencies, and no runtime validation ensured parsing consistency across security boundaries.
-
CWE: CWE-918 (Server-Side Request Forgery) with contributing factors from CWE-436 (Interpretation Conflict).
-
Fix: Upgraded ip-address to version 10.3.1 with explicit dependency declaration in package.json and strategic lock file management to ensure consistent, secure parsing behavior throughout the 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 demonstrates how subtle parsing inconsistencies in foundational libraries can create serious security vulnerabilities. The ip-address package's inconsistent interpretation of IP strings enabled SSRF attacks that could bypass trust boundaries and expose internal resources. By upgrading to version 10.3.1 and implementing explicit dependency management, this application closed a critical attack vector.
The broader lesson is that security-critical operations—like IP address validation for access control—require consistent parsing logic throughout the entire data flow. Relying on transitive dependency resolution for security libraries is risky; explicit version pinning and comprehensive dependency scanning are essential practices. Combined with defense-in-depth strategies like allowlist validation and network segmentation, these practices significantly reduce SSRF risk in modern Node.js applications.