From fa3b1f9e38489455b7289e597357ea8723df8c40 Mon Sep 17 00:00:00 2001 From: tjuhasz Date: Tue, 4 Aug 2026 17:00:55 +0200 Subject: [PATCH] Fix for CVE-2026-69192 & CVE-2026-54272 : ip-address Fix for CVE-2026-69192 & CVE-2026-54272 by rebasing ip-address dependency to 10.4.0 Advisories: https://github.com/advisories/GHSA-mwp4-54f8-5fhr(CVE-2026-69192) * Fixed in 10.3.1 https://github.com/beaugunderson/ip-address/security/advisories/GHSA-22jq-vg5j-6vgg(CVE-2026-54272) * Fixed in 10.2.1 Resolves: RHEL-223390 RHEL-223917 --- ...192-CVE-2026-54272-ip-address-10.4.0.patch | 811 ++++++++++++++++++ nodejs24.spec | 2 + 2 files changed, 813 insertions(+) create mode 100644 0003-CVE-2026-69192-CVE-2026-54272-ip-address-10.4.0.patch diff --git a/0003-CVE-2026-69192-CVE-2026-54272-ip-address-10.4.0.patch b/0003-CVE-2026-69192-CVE-2026-54272-ip-address-10.4.0.patch new file mode 100644 index 0000000..06eb81d --- /dev/null +++ b/0003-CVE-2026-69192-CVE-2026-54272-ip-address-10.4.0.patch @@ -0,0 +1,811 @@ +From caaad6f1bc0fec6d7bbabae0f6ebfd605eb885f4 Mon Sep 17 00:00:00 2001 +From: tjuhasz +Date: Tue, 4 Aug 2026 16:26:19 +0200 +Subject: [PATCH] deps: update ip-address to 10.4.0 + +Update ip-address package in deps/npm/node_modules to version 10.4.0 +to address CVE-2026-69192 and CVE-2026-54272. + +Key security fixes: +- IPv4 leading zero validation: rejects ambiguous octal notation that + could cause disagreement between parsers about which host a string + names (SSRF vector) +- isHostInSubnet: new method used by all classifiers (isLoopback, + isPrivate, isLinkLocal, etc.) so classification is no longer affected + by the CIDR suffix, preventing SSRF filter bypasses +- Improved URL parsing regexes that properly anchor and handle + protocol prefixes +- IPv6 embedded IPv4 awareness: isLoopback/isPrivate/isUnspecified + now check the embedded v4 address for mapped/NAT64 addresses + +CVE: CVE-2026-69192, CVE-2026-54272 +--- + .../node_modules/ip-address/dist/common.js | 49 +++- + deps/npm/node_modules/ip-address/dist/ipv4.js | 44 ++-- + deps/npm/node_modules/ip-address/dist/ipv6.js | 233 ++++++++++++++---- + .../ip-address/dist/v4/constants.js | 6 +- + .../ip-address/dist/v6/constants.js | 5 +- + deps/npm/node_modules/ip-address/package.json | 10 +- + 6 files changed, 262 insertions(+), 85 deletions(-) + +diff --git a/deps/npm/node_modules/ip-address/dist/common.js b/deps/npm/node_modules/ip-address/dist/common.js +index 6b76e05..0c15d21 100644 +--- a/deps/npm/node_modules/ip-address/dist/common.js ++++ b/deps/npm/node_modules/ip-address/dist/common.js +@@ -1,23 +1,47 @@ + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.isInSubnet = isInSubnet; ++exports.isHostInSubnet = isHostInSubnet; + exports.isCorrect = isCorrect; + exports.prefixLengthFromMask = prefixLengthFromMask; ++exports.assertByteArray = assertByteArray; + exports.numberToPaddedHex = numberToPaddedHex; + exports.stringToPaddedHex = stringToPaddedHex; + exports.testBit = testBit; + const address_error_1 = require("./address-error"); ++/** ++ * Returns whether this address's *network* is contained within `address`, ++ * i.e. whether every address this one can represent also falls inside ++ * `address`. A network wider than `address` is not contained in it, so ++ * `10.0.0.0/8` is not in `10.0.0.0/16`. ++ * ++ * To ask whether the address itself falls inside a range, ignoring any CIDR ++ * suffix it was written with, use {@link isHostInSubnet} instead. That is the ++ * question the special-use classifiers ask. ++ */ + function isInSubnet(address) { + if (this.subnetMask < address.subnetMask) { + return false; + } +- if (this.mask(address.subnetMask) === address.mask()) { +- return true; +- } +- return false; ++ return isHostInSubnet.call(this, address); ++} ++/** ++ * Returns whether this address's host bits fall inside `address`, ignoring ++ * this address's own subnet mask. ++ * ++ * This is the primitive the special-use classifiers (`isLoopback`, ++ * `isPrivate`, `isLinkLocal`, `getType`, …) are built on: they answer a ++ * question about the address, so the answer must not change with the CIDR ++ * suffix the caller happened to write. Use this rather than ++ * {@link isInSubnet} when classifying a single address — notably when the ++ * address came from untrusted input and the result backs a trust-boundary ++ * decision such as an SSRF allow/deny filter. ++ */ ++function isHostInSubnet(address) { ++ return this.mask(address.subnetMask) === address.mask(); + } + function isCorrect(defaultBits) { +- return function () { ++ return function isCorrectForm() { + if (this.addressMinusSuffix !== this.correctForm()) { + return false; + } +@@ -46,6 +70,21 @@ function prefixLengthFromMask(value, totalBits) { + } + return firstZero; + } ++/** ++ * Throws `AddressError` unless `bytes` holds exactly `byteCount` integers, ++ * each from `minimum` to 255. Pass a `minimum` of `-128` where signed bytes ++ * are accepted and folded to unsigned, and `0` where they are not. ++ */ ++function assertByteArray(bytes, byteCount, family, minimum) { ++ if (bytes.length !== byteCount) { ++ throw new address_error_1.AddressError(`${family} addresses require exactly ${byteCount} bytes`); ++ } ++ for (let i = 0; i < bytes.length; i++) { ++ if (!Number.isInteger(bytes[i]) || bytes[i] < minimum || bytes[i] > 255) { ++ throw new address_error_1.AddressError(`All bytes must be integers between ${minimum} and 255`); ++ } ++ } ++} + function numberToPaddedHex(number) { + return number.toString(16).padStart(2, '0'); + } +diff --git a/deps/npm/node_modules/ip-address/dist/ipv4.js b/deps/npm/node_modules/ip-address/dist/ipv4.js +index 2c0fd18..1a14ddc 100644 +--- a/deps/npm/node_modules/ip-address/dist/ipv4.js ++++ b/deps/npm/node_modules/ip-address/dist/ipv4.js +@@ -35,6 +35,7 @@ const isCorrect4 = common.isCorrect(constants.BITS); + */ + class Address4 { + constructor(address) { ++ this.addressMinusSuffix = ''; + this.groups = constants.GROUPS; + this.parsedAddress = []; + this.parsedSubnet = ''; +@@ -51,6 +52,13 @@ class Address4 { + * @returns {boolean} + */ + this.isInSubnet = common.isInSubnet; ++ /** ++ * Returns true if this address's host bits fall inside the given subnet, ++ * ignoring this address's own subnet mask. See ++ * {@link common.isHostInSubnet}. ++ * @returns {boolean} ++ */ ++ this.isHostInSubnet = common.isHostInSubnet; + this.address = address; + const subnet = constants.RE_SUBNET_STRING.exec(address); + if (subnet) { +@@ -78,7 +86,7 @@ class Address4 { + new Address4(address); + return true; + } +- catch (e) { ++ catch { + return false; + } + } +@@ -90,6 +98,11 @@ class Address4 { + */ + parse(address) { + const groups = address.split('.'); ++ // Checked before the general match so the error names the actual problem. ++ // Address6 rejects the same notation on its v4-in-v6 path. ++ if (groups.some((group) => /^0\d/.test(group))) { ++ throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes."); ++ } + if (!address.match(constants.RE_ADDRESS)) { + throw new address_error_1.AddressError('Invalid IPv4 address.'); + } +@@ -128,7 +141,6 @@ class Address4 { + static fromAddressAndWildcardMask(address, wildcardMask) { + const wildcard = new Address4(wildcardMask).bigInt(); + const allOnes = (BigInt(1) << BigInt(constants.BITS)) - BigInt(1); +- // eslint-disable-next-line no-bitwise + const mask = wildcard ^ allOnes; + const bits = common.prefixLengthFromMask(mask, constants.BITS); + return new Address4(`${address}/${bits}`); +@@ -328,7 +340,7 @@ class Address4 { + * @returns {Address4} + */ + static fromBigInt(bigInt) { +- if (bigInt < 0n || bigInt > 0xffffffffn) { ++ if (bigInt < BigInt(0) || bigInt > BigInt(0xffffffff)) { + throw new address_error_1.AddressError('IPv4 BigInt must be in the range 0 to 2**32 - 1'); + } + return Address4.fromHex(bigInt.toString(16).padStart(8, '0')); +@@ -341,15 +353,7 @@ class Address4 { + * @returns {Address4} + */ + static fromByteArray(bytes) { +- if (bytes.length !== 4) { +- throw new address_error_1.AddressError('IPv4 addresses require exactly 4 bytes'); +- } +- // Validate that all bytes are within valid range (0-255) +- for (let i = 0; i < bytes.length; i++) { +- if (!Number.isInteger(bytes[i]) || bytes[i] < 0 || bytes[i] > 255) { +- throw new address_error_1.AddressError('All bytes must be integers between 0 and 255'); +- } +- } ++ common.assertByteArray(bytes, 4, 'IPv4', 0); + return this.fromUnsignedByteArray(bytes); + } + /** +@@ -403,49 +407,49 @@ class Address4 { + * @returns {boolean} + */ + isMulticast() { +- return this.isInSubnet(MULTICAST_V4); ++ return this.isHostInSubnet(MULTICAST_V4); + } + /** + * Returns true if the address is in one of the [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private address ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). + * @returns {boolean} + */ + isPrivate() { +- return PRIVATE_V4.some((subnet) => this.isInSubnet(subnet)); ++ return PRIVATE_V4.some((subnet) => this.isHostInSubnet(subnet)); + } + /** + * Returns true if the address is in the loopback range `127.0.0.0/8` ([RFC 1122](https://datatracker.ietf.org/doc/html/rfc1122)). + * @returns {boolean} + */ + isLoopback() { +- return this.isInSubnet(LOOPBACK_V4); ++ return this.isHostInSubnet(LOOPBACK_V4); + } + /** + * Returns true if the address is in the link-local range `169.254.0.0/16` ([RFC 3927](https://datatracker.ietf.org/doc/html/rfc3927)). + * @returns {boolean} + */ + isLinkLocal() { +- return this.isInSubnet(LINK_LOCAL_V4); ++ return this.isHostInSubnet(LINK_LOCAL_V4); + } + /** + * Returns true if the address is the unspecified address `0.0.0.0`. + * @returns {boolean} + */ + isUnspecified() { +- return this.isInSubnet(UNSPECIFIED_V4); ++ return this.isHostInSubnet(UNSPECIFIED_V4); + } + /** + * Returns true if the address is the limited broadcast address `255.255.255.255` ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)). + * @returns {boolean} + */ + isBroadcast() { +- return this.isInSubnet(BROADCAST_V4); ++ return this.isHostInSubnet(BROADCAST_V4); + } + /** + * Returns true if the address is in the carrier-grade NAT range `100.64.0.0/10` ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)). + * @returns {boolean} + */ + isCGNAT() { +- return this.isInSubnet(CGNAT_V4); ++ return this.isHostInSubnet(CGNAT_V4); + } + /** + * Returns a zero-padded base-2 string representation of the address +@@ -463,7 +467,7 @@ class Address4 { + */ + groupForV6() { + const segments = this.parsedAddress; +- return this.address.replace(constants.RE_ADDRESS, `${segments ++ return this.correctForm().replace(constants.RE_ADDRESS, `${segments + .slice(0, 2) + .join('.')}.${segments + .slice(2, 4) +diff --git a/deps/npm/node_modules/ip-address/dist/ipv6.js b/deps/npm/node_modules/ip-address/dist/ipv6.js +index a78020e..f64a8dd 100644 +--- a/deps/npm/node_modules/ip-address/dist/ipv6.js ++++ b/deps/npm/node_modules/ip-address/dist/ipv6.js +@@ -73,7 +73,6 @@ function paddedHex(octet) { + return parseInt(octet, 16).toString(16).padStart(4, '0'); + } + function unsignByte(b) { +- // eslint-disable-next-line no-bitwise + return b & 0xff; + } + /** +@@ -97,6 +96,13 @@ class Address6 { + * @returns {boolean} + */ + this.isInSubnet = common.isInSubnet; ++ /** ++ * Returns true if this address's host bits fall inside the given subnet, ++ * ignoring this address's own subnet mask. See ++ * {@link common.isHostInSubnet}. ++ * @returns {boolean} ++ */ ++ this.isHostInSubnet = common.isHostInSubnet; + /** + * Returns true if the address is correct, false otherwise + * @returns {boolean} +@@ -121,7 +127,10 @@ class Address6 { + } + address = address.replace(constants6.RE_SUBNET_STRING, ''); + } +- else if (/\//.test(address)) { ++ // RE_SUBNET_STRING anchors on the end of the address, so it strips only ++ // the trailing suffix. A second one left behind (`::/0/1`) is malformed ++ // and must be rejected rather than parsed as an address group. ++ if (/\//.test(address)) { + throw new address_error_1.AddressError('Invalid subnet mask.'); + } + const zone = constants6.RE_ZONE_STRING.exec(address); +@@ -145,7 +154,7 @@ class Address6 { + new Address6(address); + return true; + } +- catch (e) { ++ catch { + return false; + } + } +@@ -160,7 +169,7 @@ class Address6 { + * address.correctForm(); // '::e8:d4a5:1000' + */ + static fromBigInt(bigInt) { +- if (bigInt < 0n || bigInt > (1n << BigInt(constants6.BITS)) - 1n) { ++ if (bigInt < BigInt(0) || bigInt > (BigInt(1) << BigInt(constants6.BITS)) - BigInt(1)) { + throw new address_error_1.AddressError('IPv6 BigInt must be in the range 0 to 2**128 - 1'); + } + const hex = bigInt.toString(16).padStart(32, '0'); +@@ -181,12 +190,15 @@ class Address6 { + * addressAndPort.port; // 8080 + */ + static fromURL(url) { ++ var _a; + let host; + let port = null; + let result; ++ // Remove the protocol prefix, if any ++ const stripped = url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ''); + // If we have brackets parse them and find a port +- if (url.indexOf('[') !== -1 && url.indexOf(']:') !== -1) { +- result = constants6.RE_URL_WITH_PORT.exec(url); ++ if (stripped.indexOf('[') !== -1 && stripped.indexOf(']:') !== -1) { ++ result = constants6.RE_URL_WITH_PORT.exec(stripped); + if (result === null) { + return { + error: 'failed to parse address with port', +@@ -196,13 +208,9 @@ class Address6 { + } + host = result[1]; + port = result[2]; +- // If there's a URL extract the address + } +- else if (url.indexOf('/') !== -1) { +- // Remove the protocol prefix +- url = url.replace(/^[a-z0-9]+:\/\//, ''); +- // Parse the address +- result = constants6.RE_URL.exec(url); ++ else { ++ result = constants6.RE_URL.exec(stripped); + if (result === null) { + return { + error: 'failed to parse address from URL', +@@ -210,17 +218,13 @@ class Address6 { + port: null, + }; + } +- host = result[1]; +- // Otherwise just assign the URL to the host and let the library parse it +- } +- else { +- host = url; ++ host = (_a = result[1]) !== null && _a !== void 0 ? _a : result[2]; + } + // If there's a port convert it to an integer + if (port) { + port = parseInt(port, 10); +- // squelch out of range ports +- if (port < 0 || port > 65536) { ++ // squelch out of range ports (valid ports are 0-65535) ++ if (port < 0 || port > 65535) { + port = null; + } + } +@@ -258,7 +262,6 @@ class Address6 { + static fromAddressAndWildcardMask(address, wildcardMask) { + const wildcard = new Address6(wildcardMask).bigInt(); + const allOnes = (BigInt(1) << BigInt(constants6.BITS)) - BigInt(1); +- // eslint-disable-next-line no-bitwise + const mask = wildcard ^ allOnes; + const bits = common.prefixLengthFromMask(mask, constants6.BITS); + return new Address6(`${address}/${bits}`); +@@ -493,7 +496,7 @@ class Address6 { + getType() { + for (let i = 0; i < TYPE_SUBNETS.length; i++) { + const entry = TYPE_SUBNETS[i]; +- if (this.isInSubnet(entry[0])) { ++ if (this.isHostInSubnet(entry[0])) { + return entry[1]; + } + } +@@ -635,20 +638,27 @@ class Address6 { + } + const groups = address.split(':'); + const lastGroup = groups.slice(-1)[0]; ++ // RE_ADDRESS rejects octets with a leading zero, so a dotted-quad tail is ++ // matched permissively first: that way this notation still gets its own ++ // message with the offending octet highlighted, rather than falling ++ // through as an unrecognized group. ++ const v4Octets = lastGroup.split('.'); ++ if (v4Octets.length === constants4.GROUPS && ++ v4Octets.every((octet) => /^\d{1,3}$/.test(octet))) { ++ if (v4Octets.some((octet) => /^0\d/.test(octet))) { ++ // The prefix groups haven't been through the bad-character check ++ // yet, so escape them before including in the error HTML. ++ const highlighted = v4Octets.map(spanLeadingZeroes4).join('.'); ++ const prefix = groups.slice(0, -1).map(helpers.escapeHtml).join(':'); ++ const separator = groups.length > 1 ? ':' : ''; ++ throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", `${prefix}${separator}${highlighted}`); ++ } ++ } + const address4 = lastGroup.match(constants4.RE_ADDRESS); + if (address4) { + this.parsedAddress4 = address4[0]; +- this.address4 = new ipv4_1.Address4(this.parsedAddress4); +- for (let i = 0; i < this.address4.groups; i++) { +- if (/^0[0-9]+/.test(this.address4.parsedAddress[i])) { +- // The prefix groups haven't been through the bad-character check +- // yet, so escape them before including in the error HTML. +- const highlighted = this.address4.parsedAddress.map(spanLeadingZeroes4).join('.'); +- const prefix = groups.slice(0, -1).map(helpers.escapeHtml).join(':'); +- const separator = groups.length > 1 ? ':' : ''; +- throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", `${prefix}${separator}${highlighted}`); +- } +- } ++ const v4Suffix = this.subnetMask >= 96 ? `/${this.subnetMask - 96}` : ''; ++ this.address4 = new ipv4_1.Address4(`${this.parsedAddress4}${v4Suffix}`); + this.v4 = true; + groups[groups.length - 1] = this.address4.toGroup6(); + address = groups.join(':'); +@@ -734,7 +744,11 @@ class Address6 { + return BigInt(`0x${this.parsedAddress.map(paddedHex).join('')}`); + } + /** +- * Return the last two groups of this address as an IPv4 address string ++ * Return the last two groups of this address as an IPv4 address string. ++ * If this address carries a CIDR prefix that covers the trailing 32 bits ++ * (i.e. `subnetMask >= 96`), the resulting `Address4` inherits the ++ * corresponding v4 prefix (`subnetMask - 96`); otherwise it defaults to ++ * `/32`. + * @returns {Address4} + * @example + * var address = new Address6('2001:4860:4001::1825:bf11'); +@@ -742,7 +756,18 @@ class Address6 { + */ + to4() { + const binary = this.binaryZeroPad().split(''); +- return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16).padStart(8, '0')); ++ const hex = BigInt(`0b${binary.slice(96, 128).join('')}`) ++ .toString(16) ++ .padStart(8, '0'); ++ if (this.subnetMask >= 96) { ++ const v4Mask = this.subnetMask - 96; ++ const groups = []; ++ for (let i = 0; i < 8; i += 2) { ++ groups.push(parseInt(hex.slice(i, i + 2), 16)); ++ } ++ return new ipv4_1.Address4(`${groups.join('.')}/${v4Mask}`); ++ } ++ return ipv4_1.Address4.fromHex(hex); + } + /** + * Return the v4-in-v6 form of the address +@@ -756,7 +781,7 @@ class Address6 { + if (!/:$/.test(correct)) { + infix = ':'; + } +- return correct + infix + address4.address; ++ return correct + infix + address4.correctForm(); + } + /** + * Decodes the Teredo tunneling fields embedded in this address. Returns the +@@ -788,11 +813,9 @@ class Address6 { + */ + const prefix = this.getBitsBase16(0, 32); + const bitsForUdpPort = this.getBits(80, 96); +- // eslint-disable-next-line no-bitwise + const udpPort = (bitsForUdpPort ^ BigInt('0xffff')).toString(); + const server4 = ipv4_1.Address4.fromHex(this.getBitsBase16(32, 64)); + const bitsForClient4 = this.getBits(96, 128); +- // eslint-disable-next-line no-bitwise + const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16).padStart(8, '0')); + const flagsBase2 = this.getBitsBase2(64, 80); + const coneNat = (0, common_1.testBit)(flagsBase2, 15); +@@ -874,12 +897,14 @@ class Address6 { + } + else { + const beforeU = 64 - pl; +- bits = +- prefixBits.slice(0, pl) + +- v4Bits.slice(0, beforeU) + +- '00000000' + +- v4Bits.slice(beforeU) + +- '0'.repeat(128 - 72 - (32 - beforeU)); ++ bits = [ ++ prefixBits.slice(0, pl), ++ v4Bits.slice(0, beforeU), ++ // Bits 64 to 71 are the reserved u octet and are always zero. ++ '00000000', ++ v4Bits.slice(beforeU), ++ '0'.repeat(128 - 72 - (32 - beforeU)), ++ ].join(''); + } + const hex = BigInt(`0b${bits}`).toString(16).padStart(32, '0'); + const groups = []; +@@ -902,7 +927,7 @@ class Address6 { + if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) { + throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96'); + } +- if (!this.isInSubnet(prefix6)) { ++ if (!this.isHostInSubnet(prefix6)) { + return null; + } + const bits = this.binaryZeroPad(); +@@ -927,9 +952,9 @@ class Address6 { + * @returns {Array} + */ + toByteArray() { +- const valueWithoutPadding = this.bigInt().toString(16); +- const leadingPad = '0'.repeat(valueWithoutPadding.length % 2); +- const value = `${leadingPad}${valueWithoutPadding}`; ++ const value = this.bigInt() ++ .toString(16) ++ .padStart(constants6.BITS / 4, '0'); + const bytes = []; + for (let i = 0, length = value.length; i < length; i += 2) { + bytes.push(parseInt(value.substring(i, i + 2), 16)); +@@ -943,24 +968,39 @@ class Address6 { + * @returns {Array} + */ + toUnsignedByteArray() { ++ // toByteArray() emits 0 to 255, so unsigning it is an identity mapping and ++ // the two methods return equal arrays. 11.0.0 keeps one of them and makes ++ // this a deprecated alias; test/common-test.ts fails at that version. + return this.toByteArray().map(unsignByte); + } + /** + * Convert a byte array to an Address6 object. + * ++ * Accepts unsigned bytes (0 to 255) or signed bytes (-128 to 127, as an ++ * `Int8Array` or a Java `byte[]` holds them), folding signed values to their ++ * unsigned equivalent. Throws `AddressError` unless given exactly 16 ++ * integers from -128 to 255. ++ * + * To convert from a Node.js `Buffer`, spread it: `Address6.fromByteArray([...buf])`. + * @returns {Address6} + */ + static fromByteArray(bytes) { ++ // Address4.fromByteArray takes unsigned bytes only. 11.0.0 aligns this ++ // method with it, at which point the -128 floor here, unsignByte, and the ++ // mapping below all go; test/common-test.ts fails at that version. ++ common.assertByteArray(bytes, 16, 'IPv6', -128); + return this.fromUnsignedByteArray(bytes.map(unsignByte)); + } + /** + * Convert an unsigned byte array to an Address6 object. + * ++ * Throws `AddressError` unless given exactly 16 integers from 0 to 255. ++ * + * To convert from a Node.js `Buffer`, spread it: `Address6.fromUnsignedByteArray([...buf])`. + * @returns {Address6} + */ + static fromUnsignedByteArray(bytes) { ++ common.assertByteArray(bytes, 16, 'IPv6', 0); + const BYTE_MAX = BigInt('256'); + let result = BigInt('0'); + let multiplier = BigInt('1'); +@@ -982,7 +1022,11 @@ class Address6 { + * @returns {boolean} + */ + isLinkLocal() { +- // Zeroes are required, i.e. we can't check isInSubnet with 'fe80::/10' ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isLinkLocal(); ++ } ++ // Zeroes are required, i.e. we can't check isHostInSubnet with 'fe80::/10' + if (this.getBitsBase2(0, 64) === + '1111111010000000000000000000000000000000000000000000000000000000') { + return true; +@@ -994,6 +1038,10 @@ class Address6 { + * @returns {boolean} + */ + isMulticast() { ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isMulticast(); ++ } + const type = this.getType(); + return type === 'Multicast' || type.startsWith('Multicast '); + } +@@ -1016,27 +1064,54 @@ class Address6 { + * @returns {boolean} + */ + isMapped4() { +- return this.isInSubnet(IPV4_MAPPED_SUBNET); ++ return this.isHostInSubnet(IPV4_MAPPED_SUBNET); ++ } ++ /** ++ * If this address embeds a routable IPv4 address — i.e. it is IPv4-mapped ++ * (`::ffff:0:0/96`) or sits in the NAT64 well-known prefix (`64:ff9b::/96`, ++ * [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052)) — return that ++ * embedded address as an {@link Address4}; otherwise return null. ++ * ++ * The special-property checks (`isLoopback`, `isLinkLocal`, `isMulticast`, ++ * `isUnspecified`, `isPrivate`, `isCGNAT`, `isBroadcast`) call this first and ++ * delegate to the embedded {@link Address4} when present, so a literal such as ++ * `::ffff:127.0.0.1` is classified by what it actually reaches (loopback) ++ * rather than by its IPv6 wrapper (which `getType()` reports as IPv4-mapped). ++ * This matters wherever the checks back a trust-boundary decision (e.g. an ++ * SSRF allow/deny filter): without normalization, `::ffff:10.0.0.1`, ++ * `::ffff:169.254.169.254`, `64:ff9b::7f00:1`, etc. would all read as ++ * non-internal. ++ * @returns {Address4 | null} ++ */ ++ embeddedIPv4() { ++ if (this.isMapped4() || this.isHostInSubnet(NAT64_WELL_KNOWN_SUBNET)) { ++ return this.to4(); ++ } ++ return null; + } + /** + * Returns true if the address is a Teredo address, false otherwise + * @returns {boolean} + */ + isTeredo() { +- return this.isInSubnet(TEREDO_SUBNET); ++ return this.isHostInSubnet(TEREDO_SUBNET); + } + /** + * Returns true if the address is a 6to4 address, false otherwise + * @returns {boolean} + */ + is6to4() { +- return this.isInSubnet(SIX_TO_FOUR_SUBNET); ++ return this.isHostInSubnet(SIX_TO_FOUR_SUBNET); + } + /** + * Returns true if the address is a loopback address, false otherwise + * @returns {boolean} + */ + isLoopback() { ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isLoopback(); ++ } + return this.getType() === 'Loopback'; + } + /** +@@ -1044,13 +1119,64 @@ class Address6 { + * @returns {boolean} + */ + isULA() { +- return this.isInSubnet(ULA_SUBNET); ++ return this.isHostInSubnet(ULA_SUBNET); ++ } ++ /** ++ * Returns true if the address is private, i.e. a Unique Local Address in ++ * `fc00::/7` ([RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193)) or an ++ * IPv4-mapped / NAT64 address whose embedded IPv4 address is in one of the ++ * [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private ranges ++ * (e.g. `::ffff:10.0.0.1`). This is the IPv6 counterpart to ++ * {@link Address4.isPrivate}; use it instead of {@link isULA} when you need to ++ * catch mapped RFC 1918 addresses as well as native ULAs. ++ * @returns {boolean} ++ */ ++ isPrivate() { ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isPrivate(); ++ } ++ return this.isULA(); ++ } ++ /** ++ * Returns true if the address is an IPv4-mapped / NAT64 address whose embedded ++ * IPv4 address is in the carrier-grade NAT range `100.64.0.0/10` ++ * ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)), false ++ * otherwise. There is no native IPv6 CGNAT range, so this only ever returns ++ * true for an embedded IPv4 address (e.g. `::ffff:100.64.0.1`). ++ * @returns {boolean} ++ */ ++ isCGNAT() { ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isCGNAT(); ++ } ++ return false; ++ } ++ /** ++ * Returns true if the address is an IPv4-mapped / NAT64 address whose embedded ++ * IPv4 address is the limited broadcast address `255.255.255.255` ++ * ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)), false otherwise. ++ * There is no IPv6 broadcast, so this only ever returns true for an embedded ++ * IPv4 address (e.g. `::ffff:255.255.255.255`). ++ * @returns {boolean} ++ */ ++ isBroadcast() { ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isBroadcast(); ++ } ++ return false; + } + /** + * Returns true if the address is the unspecified address `::`. + * @returns {boolean} + */ + isUnspecified() { ++ const embedded = this.embeddedIPv4(); ++ if (embedded) { ++ return embedded.isUnspecified(); ++ } + return this.getType() === 'Unspecified'; + } + /** +@@ -1058,7 +1184,7 @@ class Address6 { + * @returns {boolean} + */ + isDocumentation() { +- return this.isInSubnet(DOCUMENTATION_SUBNET); ++ return this.isHostInSubnet(DOCUMENTATION_SUBNET); + } + // #endregion + // #region HTML +@@ -1214,4 +1340,5 @@ const SIX_TO_FOUR_SUBNET = new Address6('2002::/16'); + const ULA_SUBNET = new Address6('fc00::/7'); + const DOCUMENTATION_SUBNET = new Address6('2001:db8::/32'); + const IPV4_MAPPED_SUBNET = new Address6('::ffff:0:0/96'); ++const NAT64_WELL_KNOWN_SUBNET = new Address6('64:ff9b::/96'); + //# sourceMappingURL=ipv6.js.map +\ No newline at end of file +diff --git a/deps/npm/node_modules/ip-address/dist/v4/constants.js b/deps/npm/node_modules/ip-address/dist/v4/constants.js +index 6fa2518..158288b 100644 +--- a/deps/npm/node_modules/ip-address/dist/v4/constants.js ++++ b/deps/npm/node_modules/ip-address/dist/v4/constants.js +@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); + exports.RE_SUBNET_STRING = exports.RE_ADDRESS = exports.GROUPS = exports.BITS = void 0; + exports.BITS = 32; + exports.GROUPS = 4; +-exports.RE_ADDRESS = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/g; ++// Each octet is 0-255 written without a leading zero. A leading zero is ++// octal to the WHATWG URL parser, inet_aton, and getaddrinfo, but decimal to ++// parseInt(part, 10), so accepting the notation would make this library ++// disagree with the network stack about which host a string names. ++exports.RE_ADDRESS = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/g; + exports.RE_SUBNET_STRING = /\/\d{1,2}$/; + //# sourceMappingURL=constants.js.map +\ No newline at end of file +diff --git a/deps/npm/node_modules/ip-address/dist/v6/constants.js b/deps/npm/node_modules/ip-address/dist/v6/constants.js +index 1a8cd1d..4616cad 100644 +--- a/deps/npm/node_modules/ip-address/dist/v6/constants.js ++++ b/deps/npm/node_modules/ip-address/dist/v6/constants.js +@@ -44,6 +44,7 @@ exports.TYPES = { + 'ff05::1:3/128': 'Multicast (All DHCP servers in this site)', + '::/128': 'Unspecified', + '::1/128': 'Loopback', ++ '::ffff:0:0/96': 'IPv4-mapped', + 'ff00::/8': 'Multicast', + 'fe80::/10': 'Link-local unicast', + 'fc00::/7': 'Unique local', +@@ -76,6 +77,6 @@ exports.RE_SUBNET_STRING = /\/\d{1,3}(?=%|$)/; + * @static + */ + exports.RE_ZONE_STRING = /%.*$/; +-exports.RE_URL = /^\[{0,1}([0-9a-f:]+)\]{0,1}/; +-exports.RE_URL_WITH_PORT = /\[([0-9a-f:]+)\]:([0-9]{1,5})/; ++exports.RE_URL = /^(?:\[([0-9a-f:.]+)\]|([0-9a-f:.]+))(?:[/?#].*)?$/i; ++exports.RE_URL_WITH_PORT = /^\[([0-9a-f:.]+)\]:([0-9]{1,5})(?:[/?#].*)?$/i; + //# sourceMappingURL=constants.js.map +\ No newline at end of file +diff --git a/deps/npm/node_modules/ip-address/package.json b/deps/npm/node_modules/ip-address/package.json +index 47d109e..dc778c2 100644 +--- a/deps/npm/node_modules/ip-address/package.json ++++ b/deps/npm/node_modules/ip-address/package.json +@@ -16,7 +16,7 @@ + "bigint", + "browser" + ], +- "version": "10.2.0", ++ "version": "10.4.0", + "author": "Beau Gunderson (https://beaugunderson.com/)", + "license": "MIT", + "main": "dist/ip-address.js", +@@ -25,6 +25,9 @@ + "docs": "tsx scripts/build-readme.ts", + "build": "rm -rf dist; mkdir dist; tsc", + "prepack": "npm run docs && npm run build", ++ "prepare": "git config core.hooksPath hooks || true", ++ "lint": "prettier --check . && eslint . --ext .ts,.js --max-warnings 0", ++ "lint:fix": "prettier --write . && eslint . --ext .ts,.js --max-warnings 0 --fix", + "test-ci": "c8 --experimental-monocart mocha", + "test": "mocha", + "watch": "mocha --watch" +@@ -54,7 +57,7 @@ + ], + "repository": { + "type": "git", +- "url": "git://github.com/beaugunderson/ip-address.git" ++ "url": "https://github.com/beaugunderson/ip-address.git" + }, + "overrides": { + "diff": "^8.0.3", +@@ -70,11 +73,10 @@ + "chai": "^6.2.2", + "eslint": "^8.57.1", + "eslint_d": "^14.0.4", +- "eslint-config-airbnb": "^19.0.4", ++ "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-filenames": "^1.3.2", + "eslint-plugin-import": "^2.32.0", +- "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", + "mocha": "^11.7.5", +-- +2.54.0 + diff --git a/nodejs24.spec b/nodejs24.spec index ac29a86..45d80c9 100644 --- a/nodejs24.spec +++ b/nodejs24.spec @@ -157,6 +157,8 @@ Source101: nodejs.srpm.macros 0001-CVE-2026-59873-CVE-2026-59874-upgrade-bundled-tar-to-7.5.19.patch # Fix security vulnerability in braces-expansion deps 0002-CVE-2026-69152-brace-expansion-5.0.9.patch +# Fix security vulnerability in ip-address package +0003-CVE-2026-69192-CVE-2026-54272-ip-address-10.4.0.patch %description Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications.