From bd3366ed1f35f844060a4af148a56c63cec9d587 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 11 May 2026 09:29:15 +0200 Subject: [PATCH] dns,net: reject hostnames with embedded NUL bytes Ref: https://hackerone.com/reports/3656716 PR-URL: https://github.com/nodejs-private/node-private/pull/868 Reviewed-By: Antoine du Hamel CVE-ID: CVE-2026-48930 Refs: https://hackerone.com/reports/3656716 origin: backport, https://github.com/nodejs/node/commit/c551a51d0c58dfc91961fb3f24c2c86af6183eca Bug: https://nodejs.org/en/blog/vulnerability/june-2026-security-releases#embedded-nul-hostnames-can-lead-to-silent-authority-rebinding-due-to-c-string-trunc Gbp-Pq: Topic sec Gbp-Pq: Name CVE-2026-48930.patch --- lib/dns.js | 4 ++-- lib/internal/dns/promises.js | 3 ++- lib/internal/validators.js | 10 ++++++++++ lib/net.js | 3 +++ test/parallel/test-dns-lookup.js | 11 +++++++++++ test/parallel/test-net-connect-options-invalid.js | 13 +++++++++++++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/dns.js b/lib/dns.js index ca932ad05..892d3bf81 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -81,7 +81,7 @@ const { validateNumber, validateOneOf, validatePort, - validateString, + validateStringWithoutNullBytes, } = require('internal/validators'); const { @@ -145,7 +145,7 @@ function lookup(hostname, options, callback) { // Parse arguments if (hostname) { - validateString(hostname, 'hostname'); + validateStringWithoutNullBytes(hostname, 'hostname'); } if (typeof options === 'function') { diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js index 1169b2735..3fede4f82 100644 --- a/lib/internal/dns/promises.js +++ b/lib/internal/dns/promises.js @@ -65,6 +65,7 @@ const { validateOneOf, validatePort, validateString, + validateStringWithoutNullBytes, } = require('internal/validators'); const kPerfHooksDnsLookupContext = Symbol('kPerfHooksDnsLookupContext'); @@ -186,7 +187,7 @@ function lookup(hostname, options) { // Parse arguments if (hostname) { - validateString(hostname, 'hostname'); + validateStringWithoutNullBytes(hostname, 'hostname'); } if (typeof options === 'number') { diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 2f24f6f78..b860006df 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -15,6 +15,7 @@ const { ObjectPrototypeHasOwnProperty, RegExpPrototypeExec, String, + StringPrototypeIncludes, StringPrototypeToUpperCase, StringPrototypeTrim, } = primordials; @@ -162,6 +163,14 @@ function validateString(value, name) { throw new ERR_INVALID_ARG_TYPE(name, 'string', value); } +/** @type {validateString} */ +const validateStringWithoutNullBytes = hideStackFrames((value, name) => { + validateString(value, name); + if (StringPrototypeIncludes(value, '\u0000')) { + throw new ERR_INVALID_ARG_VALUE(name, value, 'must be a string without null bytes'); + } +}); + /** * @callback validateNumber * @param {*} value @@ -563,6 +572,7 @@ module.exports = { validatePort, validateSignalName, validateString, + validateStringWithoutNullBytes, validateUint32, validateUndefined, validateUnion, diff --git a/lib/net.js b/lib/net.js index be4a43589..d4e725084 100644 --- a/lib/net.js +++ b/lib/net.js @@ -122,6 +122,7 @@ const { validateNumber, validatePort, validateString, + validateStringWithoutNullBytes, } = require('internal/validators'); const kLastWriteQueueSize = Symbol('lastWriteQueueSize'); const { @@ -1265,6 +1266,8 @@ function lookupAndConnect(self, options) { const host = options.host || 'localhost'; let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options; + validateStringWithoutNullBytes(host, 'options.host'); + if (localAddress && !isIP(localAddress)) { throw new ERR_INVALID_IP_ADDRESS(localAddress); } diff --git a/test/parallel/test-dns-lookup.js b/test/parallel/test-dns-lookup.js index a847a91d6..56c55592d 100644 --- a/test/parallel/test-dns-lookup.js +++ b/test/parallel/test-dns-lookup.js @@ -23,6 +23,17 @@ const dnsPromises = dns.promises; assert.throws(() => dnsPromises.lookup(1, {}), err); } +{ + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The argument 'hostname' must be a string without null bytes\./, + }; + + assert.throws(() => dns.lookup('127.0.0.1\u0000.allowed.example', {}), err); + assert.throws(() => dnsPromises.lookup('127.0.0.1\u0000.allowed.example', {}), err); +} + // This also verifies different expectWarning notations. common.expectWarning({ // For 'internal/test/binding' module. diff --git a/test/parallel/test-net-connect-options-invalid.js b/test/parallel/test-net-connect-options-invalid.js index 05a565463..a5b54b490 100644 --- a/test/parallel/test-net-connect-options-invalid.js +++ b/test/parallel/test-net-connect-options-invalid.js @@ -25,3 +25,16 @@ const net = require('net'); }); }); } + +{ + assert.throws(() => { + net.createConnection({ + host: '127.0.0.1\u0000.allowed.example', + port: 8080, + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The property 'options\.host' must be a string without null bytes\./, + }); +} -- 2.39.5