Back to Blog
high SEVERITY9 min read

How SSRF via inconsistent IP address parsing happens in Node.js dependencies and how to fix it

A high-severity flaw (CVE-2026-69192) in the widely-used `ip-address` npm package meant that IP strings could be parsed inconsistently compared to the OS resolver and Node's own networking stack — letting an attacker slip a private/loopback address past an allowlist that used `Address4`/`Address6` for validation. This PR pins and upgrades `ip-address` from `10.1.0` to `10.3.1` in both `package.json` (via `overrides`) and `package-lock.json`, eliminating the parser divergence across the whole dep

O
By Orbis AppSec
Published August 31, 2026Reviewed August 31, 2026

Answer Summary

CVE-2026-69192 is a high-severity vulnerability in the Node.js `ip-address` package (versions before 10.3.1) where IP address strings are parsed inconsistently with the platform's real resolver, allowing SSRF and trust-boundary bypass (CWE-1286 / CWE-918). Code that used `new Address4(host)` or `new Address6(host)` to decide whether a URL pointed at a public host could be tricked into approving a request that Node then actually sent to `127.0.0.1` or a link-local metadata endpoint. The fix is to upgrade `ip-address` to `10.3.1` — here done with a `"ip-address": "10.3.1"` entry in `package.json` `overrides` plus the matching `package-lock.json` bump so transitive consumers (such as `socks`/`socks-proxy-agent`) also get the patched parser.

Vulnerability at a Glance

cweCWE-1286 (Improper Validation of Syntactic Correctness of Input), related to CWE-918 (SSRF) and CWE-20
fixUpgrade `ip-address` to `10.3.1` and enforce it tree-wide with an `overrides` entry in `package.json`
riskAn attacker-supplied host string is classified as "public" by `ip-address` but resolved to loopback, RFC1918, or cloud metadata (169.254.169.254) by Node — enabling internal network access and credential theft
languageJavaScript / TypeScript (Node.js)
root cause`ip-address` 10.1.0 accepted and normalized IP forms differently from `net.isIP()`/`getaddrinfo()`, so validation and connection disagreed on the same string
vulnerabilityServer-Side Request Forgery / trust-boundary bypass via inconsistent IP address parsing

Answer Summary

CVE-2026-69192 is a high-severity vulnerability in the Node.js ip-address package (versions before 10.3.1) where IP address strings are parsed inconsistently with the platform's real resolver, allowing SSRF and trust-boundary bypass (CWE-1286 / CWE-918). Code that used new Address4(host) or new Address6(host) to decide whether a URL pointed at a public host could be tricked into approving a request that Node then actually sent to 127.0.0.1 or a link-local metadata endpoint. The fix is to upgrade ip-address to 10.3.1 — here done with a "ip-address": "10.3.1" entry in package.json overrides plus the matching package-lock.json bump so transitive consumers (such as socks/socks-proxy-agent) also get the patched parser.


Introduction

This vulnerability could have allowed an attacker to make your server talk to itself.

Not through a bug in your route handlers, and not through a misconfigured proxy — through a parser disagreement. Deep in this project's dependency tree, at package-lock.json line 630, sat this entry:

"node_modules/ip-address": {
  "version": "10.1.0",
  "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
  "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
  "license": "MIT",
  "engines": {
    "node": ">= 12"
  }
}

ip-address is the de facto standard IPv4/IPv6 parsing library for Node. It exports the Address4 and Address6 classes and it is pulled in transitively by an enormous slice of the ecosystem — socks, socks-proxy-agent, pac-resolver, various proxy and networking agents, and hand-rolled SSRF guards in application code. It has one job: turn a string into a structured, comparable IP address.

Trivy flagged version 10.1.0 under CVE-2026-69192: "Inconsistent IP address parsing leads to Server-Side Request Forgery (SSRF) and trust-boundary bypass."

The assessment on this repository was honest — "Present in dependency tree, not confirmed reachable." That is still worth fixing, and this post explains exactly why a parsing library becomes a security boundary the moment anything in your stack uses it to answer the question "is this host safe to connect to?"

The Vulnerability Explained

The shape of the bug

SSRF defenses in Node almost always follow the same three steps:

  1. Take an untrusted URL or host from a request.
  2. Parse the host as an IP and check whether it falls in a forbidden range (loopback, RFC1918, link-local, ULA).
  3. If it looks public, make the outbound request.

Here's the pattern, written with ip-address the way thousands of projects write it:

// Representative SSRF guard built on ip-address 10.1.0
import { Address4, Address6 } from 'ip-address';

const BLOCKED_V4 = [
  new Address4('127.0.0.0/8'),
  new Address4('10.0.0.0/8'),
  new Address4('172.16.0.0/12'),
  new Address4('192.168.0.0/16'),
  new Address4('169.254.0.0/16'), // cloud metadata
];

function isSafeHost(host) {
  try {
    const v4 = new Address4(host);                 // <-- the trust decision
    return !BLOCKED_V4.some((net) => v4.isInSubnet(net));
  } catch {
    try {
      const v6 = new Address6(host);
      return !v6.isLoopback() && !v6.isLinkLocal();
    } catch {
      return true; // "not an IP, must be a hostname" — already shaky
    }
  }
}

// Step 3: the request is made with the ORIGINAL STRING, not the parsed object
if (isSafeHost(new URL(userUrl).hostname)) {
  await fetch(userUrl);
}

Look carefully at the last block. The validator consumes host through ip-address's parser. The connector (fetchundicinet.connectgetaddrinfo) consumes the same string through a completely different parser written in C.

That gap is the entire vulnerability. CVE-2026-69192 exists because ip-address 10.1.0's parser and the platform's parser did not agree on the meaning of every input string. Whenever ip-address says "this is 0.177.0.1, clearly public" and the OS says "this is 127.0.0.1, that's your own loopback interface", the check is bypassed.

Why parsers disagree

IP notation is far messier than the four-dotted-decimal-octets model most developers carry in their heads. Historical inet_aton() semantics — which glibc, musl, and Windows all inherit in one form or another — accept forms that a strict, spec-modern parser rejects or normalizes differently:

Input string Strict/library interpretation Platform resolver interpretation
0177.0.0.1 often rejected, or read as decimal 177 octal127.0.0.1
0x7f.0.0.1 rejected as non-numeric hex127.0.0.1
2130706433 not a valid dotted quad 32-bit integer → 127.0.0.1
127.1 invalid (only two parts) short form → 127.0.0.1
::ffff:127.0.0.1 an IPv6 address; IPv4 loopback checks never run dual-stack → connects to IPv4 loopback
::ffff:7f00:1 hex IPv4-mapped form; easy to miss in a loopback check same → 127.0.0.1
127.0.0.1 / 127.0.0.1. trailing/leading characters may be trimmed silently trimmed too — but only sometimes, and differently

Any single one of these being handled differently by ip-address than by getaddrinfo() is enough to defeat the guard above. This class of divergence is well-documented (it's the same family of bug as the 2021 netmask and Node ip package advisories), and CVE-2026-69192 is the ip-address 10.x instance of it.

Concrete attack scenario

Assume this application exposes any feature that fetches a user-supplied URL — a webhook tester, a link preview generator, an avatar-by-URL importer, an OpenAPI schema importer, a SOCKS-proxied outbound call. That's the reachable surface.

  1. The attacker POSTs {"url": "http://0177.0.0.1:8080/admin/keys"}.
  2. new URL(...).hostname yields 0177.0.0.1.
  3. new Address4('0177.0.0.1') in 10.1.0 does not normalize this to loopback. isInSubnet(127.0.0.0/8) returns false. isSafeHost() returns true.
  4. fetch('http://0177.0.0.1:8080/admin/keys') is executed. Node hands the string to the OS, which reads 0177 as octal and connects to 127.0.0.1:8080 — the internal admin service that trusts anything arriving from localhost.
  5. The response body is returned to the attacker.

Swap step 1 for http://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/ and, on a cloud instance, the same bypass yields temporary IAM credentials. That is the "trust-boundary bypass" half of the advisory title: services that grant privileges based on "the request came from inside the network" are now reachable by outsiders.

The socks / socks-proxy-agent angle matters too. Those packages use ip-address to decide whether a destination is already a literal IP (send as an address atom) or a hostname (send as a domain atom for remote resolution). A parse disagreement there changes which machine performs resolution, which can route traffic past egress controls entirely — no application code required.

The Fix

The fix is a dependency upgrade, applied in two coordinated places.

Change 1 — package-lock.json: the actual installed bytes

     "node_modules/ip-address": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
-      "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+      "version": "10.3.1",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
+      "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
       "license": "MIT",
       "engines": {
         "node": ">= 12"
       }
     }

Three fields changed, and all three matter:

  • version10.3.1 contains the hardened parser, which rejects (rather than silently mis-normalizes) the ambiguous forms above.
  • resolved — points npm at the new tarball URL.
  • integrity — a new Subresource Integrity hash. Editing version without updating integrity produces an EINTEGRITY failure on npm ci. This hash is what guarantees the bytes installed in CI are the reviewed 10.3.1 bytes and not a substituted tarball.

Note the engines block is unchanged (node >= 12), so there is no runtime floor being raised — this upgrade will not break anyone's Node version.

Change 2 — package.json: enforcing it across the whole tree

   "overrides": {
     "fast-uri": "4.1.2",
-    "hono": "4.12.34"
+    "hono": "4.12.34",
+    "ip-address": "10.3.1"
   }

This is the important half, and it's easy to under-appreciate.

ip-address almost certainly is not a direct dependency of this project. It arrives transitively — most likely via a SOCKS or proxy agent. That means a plain npm update or a lockfile edit can be undone the next time an unrelated dependency is bumped and npm re-resolves the tree: if socks@2.x declares "ip-address": "^10.0.0", npm is free to install 10.1.0 again.

The overrides field is npm's mechanism (npm 8.3+) for saying "no matter who asks for ip-address, and no matter what range they ask for, they get exactly 10.3.1." It makes the fix durable rather than incidental. The project already uses this pattern for fast-uri and hono, so adding ip-address is consistent with the existing remediation strategy in this repo.

Together the two files give you:

File Role
package.json overrides Policy — the version constraint that survives future re-resolution
package-lock.json Fact — the exact version + integrity hash installed by npm ci today

Verifying the fix

# Confirm only 10.3.1 is present, including transitively
npm ls ip-address --all

# Expect: no output / clean
npm audit --production

# Re-run the scanner that found it
trivy fs --scanners vuln --severity HIGH,CRITICAL .

A quick runtime sanity check that the new parser is stricter:

import { Address4 } from 'ip-address';

for (const s of ['0177.0.0.1', '0x7f.0.0.1', '2130706433', '127.1']) {
  try {
    console.log(s, '=>', new Address4(s).correctForm());
  } catch (e) {
    console.log(s, '=> REJECTED:', e.message);
  }
}

On 10.3.1 you want ambiguous forms to throw, not to quietly produce a "public-looking" address. A thrown error is a safe outcome, because it forces your code down the catch branch where you can fail closed.

Behavior preservation

The upgrade is a patch/minor bump within the same major (10.x), the engines constraint is untouched, and the public API (Address4, Address6, isInSubnet, correctForm, etc.) is unchanged. Well-formed addresses — 192.168.1.10, 2001:db8::1, 10.0.0.0/8 — parse identically before and after. Only ambiguous, encoding-abusing inputs change behavior, which is precisely the intent.

Prevention & Best Practices

Upgrading closes this CVE. Architecting differently closes the whole class.

1. Validate the resolved address, not the string

The root design flaw in the vulnerable pattern is validating one representation and connecting with another. Break that gap:

import dns from 'node:dns/promises';
import net from 'node:net';

const blocked = new net.BlockList();
blocked.addSubnet('127.0.0.0', 8);          // loopback
blocked.addSubnet('10.0.0.0', 8);           // RFC1918
blocked.addSubnet('172.16.0.0', 12);
blocked.addSubnet('192.168.0.0', 16);
blocked.addSubnet('169.254.0.0', 16);       // link-local / metadata
blocked.addSubnet('100.64.0.0', 10);        // CGNAT
blocked.addAddress('::1', 'ipv6');
blocked.addSubnet('fc00::', 7, 'ipv6');     // ULA
blocked.addSubnet('fe80::', 10, 'ipv6');    // link-local v6

async function resolveSafely(hostname) {
  const records = await dns.lookup(hostname, { all: true, verbatim: true });
  for (const { address, family } of records) {
    const type = family === 6 ? 'ipv6' : 'ipv4';
    if (blocked.check(address, type)) {
      throw new Error(`Blocked destination: ${address}`);
    }
  }
  return records[0].address; // connect to THIS, not to the original string
}

net.BlockList and net.isIP() ship with Node and are backed by the same address handling as Node's networking layer — so validator and connector cannot disagree. This is the single highest-value change you can make.

2. Fail closed on parse errors

The vulnerable snippet earlier ends with return true; // must be a hostname. Invert it. If you cannot confidently parse and classify a destination, reject it. A hardened parser that throws on 0177.0.0.1 only helps you if your catch block denies rather than allows.

3. Defend against DNS rebinding and redirects

Even perfect parsing loses to time-of-check/time-of-use: a hostname that resolves public during validation can resolve to 127.0.0.1 milliseconds later when the socket opens. Mitigate by connecting to the pinned resolved IP with an explicit Host header, and by re-validating every hop of a redirect chain (maxRedirects: 0 and handle 3xx yourself is the

Frequently Asked Questions

What is SSRF via inconsistent IP address parsing?

It's a vulnerability where a security check parses an IP or host string with one parser (here, the `ip-address` npm library) while the actual network request is made using a different parser (Node's `net`/DNS layer). If the two disagree about what `0177.0.0.1` or `::ffff:127.0.0.1` means, an attacker can pass the check and still reach an internal service. That's Server-Side Request Forgery plus a trust-boundary bypass.

How do you prevent SSRF via inconsistent IP parsing in Node.js?

Never validate a hostname string and then hand the *string* to your HTTP client. Resolve the host yourself with `dns.lookup()`, validate the returned binary addresses with Node's built-in `net.isIP()`/`net.BlockList`, and connect to the validated IP with an explicit `Host` header — or use a network-level egress allowlist so a parser bug cannot become a breach. Also keep `ip-address` at `10.3.1` or later.

What CWE is inconsistent IP address parsing?

The parsing defect itself maps to CWE-1286 (Improper Validation of Syntactic Correctness of Input), and the exploitable outcome in this case is CWE-918 (Server-Side Request Forgery). CWE-20 (Improper Input Validation) is the broader parent class.

Is blocking `127.0.0.1` and `10.0.0.0/8` by string matching enough to prevent this?

No. String-based denylists are exactly what this CVE defeats. Alternate encodings (leading-zero octets, IPv4-mapped IPv6, integer notation), DNS names that resolve to private space, and redirects that hop to `169.254.169.254` all bypass string matching. Validate the resolved binary address, not the text.

Can static analysis detect this vulnerability?

Yes for the dependency dimension — SCA scanners like Trivy match the installed `ip-address` version in `package-lock.json` against CVE-2026-69192, which is exactly how Orbis AppSec found this. Detecting whether your own code *reaches* the vulnerable parser requires taint analysis from a request parameter to `new Address4()`/`new Address6()` or to a SOCKS/HTTP agent.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #83

Related Articles

high

How Octal IP Address Parsing Leads to SSRF in Node.js and How to Fix It

CVE-2026-69192 reveals a critical inconsistency in the `ip-address` library where Address4 decodes leading-zero octets as decimal while DNS resolvers interpret them as octal, creating a dangerous parsing divergence. This mismatch allows attackers to bypass IP-based access controls and perform Server-Side Request Forgery (SSRF) attacks. The fix upgrades `ip-address` from 9.0.5 to 10.3.1, aligning parsing behavior with standard resolver implementations.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js fetch wrappers and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in the `recon.mjs` script, where a fetch wrapper accepted arbitrary URLs without validation. This allowed attackers to access internal infrastructure and cloud metadata services. The fix implements comprehensive URL validation that blocks internal IP ranges, loopback addresses, and dangerous protocols before any network request is made.

high

How IP Address Parsing Inconsistencies Cause SSRF and Trust-Boundary Bypass in Node.js Applications

The `ip-address` library version 10.2.0 contained a critical parsing inconsistency where the `Address4` decoder interpreted leading-zero octets as decimal numbers, while most DNS resolvers and network systems interpreted them as octal. This mismatch allowed attackers to bypass IP-based access controls and SSRF filters. Upgrading to version 10.3.1 fixes this vulnerability by aligning the library's parsing behavior with standard resolver behavior.

critical

How Server-Side Request Forgery (SSRF) Happens in Node.js fetch Tools and How to Fix It

A critical Server-Side Request Forgery (SSRF) vulnerability in `plugins/tools/fetch.js` allowed attackers to access internal resources and cloud metadata endpoints by passing arbitrary URLs to the fetch command. The fix adds hostname resolution and private IP range validation before executing any HTTP requests, preventing attackers from targeting internal infrastructure.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js API proxies and how to fix it

A critical SSRF vulnerability was discovered in server.js where the API proxy endpoint constructed target URLs from user-controlled path parameters without validating the final origin. Attackers could use URL encoding tricks like `/api/%2F%2Fevil.com` to redirect proxy requests to arbitrary hosts, potentially accessing cloud metadata services or internal resources. The fix adds origin validation to ensure all proxied requests only reach the intended openrouter.ai upstream.

high

How Dependabot Missing Cooldown Periods Enable Supply Chain Attacks and How to Fix It

A critical security vulnerability in `.github/dependabot.yml` was exposing a Node.js library to supply chain attacks by automatically updating to newly published packages without a safety delay. By adding a 7-day cooldown period to each package ecosystem configuration, the project now protects against malicious or unstable package versions that could affect downstream consumers.