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 and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #83

Related Articles

medium

How gitlab.bandit.B501 happens in Python and how to fix it

The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.

high

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

critical

How SSRF via Vulnerable Dependency Versions Happens in Node.js and How to Fix It

A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.

critical

How Server-Side Request Forgery happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

critical

How Server-Side Request Forgery happens in Node.js maintenance scripts and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi

high

modelExporter.js Path Traversal via Unsanitized Directory Concatenation

A path traversal vulnerability in `modelExporter.js` allowed attackers to read arbitrary files by injecting traversal sequences into directory and relative path parameters. The `readSourceFile` function concatenated these unsanitized inputs directly into file URLs passed to `fetch()`. The fix introduces strict path normalization that rejects attempts to escape the intended directory.