From 08e26eedbb4a4da835579c35512305044987840f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gera=20G=2E=20G=C3=BCiles?= Date: Fri, 23 Oct 2026 21:36:06 +0200 Subject: [PATCH 1/2] Hardened hostname handling against Node.js October 2026 security release Node.js published security releases on 2026-10-21 (22.23.4, 24.22.1, 26.12.1). Several of the fixed issues depend on how a hostname is written before it reaches the DNS resolver or the TLS identity check. Total.js cannot require a specific Node.js version, so the framework now normalizes and validates hostnames itself, whatever Node.js version the user runs. Related advisories: - CVE-2026-105482 (high): dns.resolve*() truncates names at a NUL byte, so "attacker.example\0.trusted.example" can pass a hostname validation and then resolve a different name. - CVE-2026-105483 (high), CVE-2026-106069 (high), CVE-2026-106072 (medium): tls.checkServerIdentity() mishandles IPv4 literals written with non-ASCII digits, leading zeros (octal vs decimal) or non-canonical forms (0x7f.0.0.1, 127.1, 2130706433), so a certificate can match a different address than the one the OS connects to. Unlike Total.js 5, this framework parses URLs with the legacy Url.parse(), which keeps "0127.0.0.1" or "0x7f.1" exactly as written, so these values reached the resolver and the TLS check untouched. A redirect (Location header) could also point a request to such an address. Changes: - utils.js: added U.validhost(host). It accepts plain hostnames and canonical IP literals only and rejects NUL bytes, non-ASCII characters and non-canonical IPv4 forms. - utils.js: added U.tohost(host). It converts the host to its canonical ASCII form with url.domainToASCII (punycode for international names, dotted IPv4 for numeric forms) and returns an empty string when the host is invalid. Valid input keeps working: only the written form is normalized. - utils.js: REQUEST(), U.resolve() and the redirects normalize the hostname of the parsed URL and return "Invalid hostname" when it is not valid. - utils.js: U.connect() (also used by LDAP()) normalizes opt.host and returns an "Invalid hostname" error instead of connecting when it is not valid. - websocketclient.js: the host is normalized and an invalid hostname emits the error event. - mail.js: the SMTP server is normalized with U.tohost(). Invalid values return "Invalid SMTP server hostname." instead of reaching the socket. Not covered by the framework (fixed only by updating Node.js): - CVE-2026-105481 (medium): unsolicited data on a reused keep-alive socket. - The Brotli decoder issue (CVE-2026-111497) does not apply, the framework only decompresses gzip and deflate. The proxy host is taken from configuration and its certificate is not validated, so it was left as it is. Tested with the framework test suite on Node.js 14, 16, 18, 19, 20, 22, 24 and 26. --- mail.js | 11 ++++++ utils.js | 88 +++++++++++++++++++++++++++++++++++++++++++++- websocketclient.js | 8 ++++- 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/mail.js b/mail.js index 7e976bc..cea8835 100755 --- a/mail.js +++ b/mail.js @@ -601,6 +601,17 @@ Mailer.prototype.send = function(smtp, options, messages, callback, cache) { options = {}; } + if (smtp && typeof(smtp) === 'string') { + var smtphost = framework_utils.tohost(smtp); + if (!smtphost) { + var smtperr = new Error('Invalid SMTP server hostname.'); + callback && callback(smtperr); + F.error(smtperr, 'mail_smtp'); + return this; + } + smtp = smtphost; + } + if (cache && CONNECTIONS[smtp]) { if (messages instanceof Array) { diff --git a/utils.js b/utils.js index 67bf618..e4d44dc 100755 --- a/utils.js +++ b/utils.js @@ -316,12 +316,63 @@ exports.toURLEncode = function(value) { return builder.length ? builder.join('&') : ''; }; +const REG_HOST_NUMERIC = /^(0x[0-9a-f]+|[0-9]+)(\.(0x[0-9a-f]+|[0-9]+))*$/i; +const REG_HOST_IPV4 = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/; + +// Accepts plain hostnames and canonical IP literals only. It rejects NUL bytes, non-ASCII characters +// and non-canonical IPv4 forms (0127.0.0.1, 0x7f.1, 2130706433, 127.1) which Node.js, certificate +// checks and the OS can interpret differently. +exports.validhost = function(host) { + + if (!host || typeof(host) !== 'string' || /[^\x21-\x7e]/.test(host)) + return false; + + return REG_HOST_NUMERIC.test(host) ? REG_HOST_IPV4.test(host) : true; +}; + +// Converts a host to its canonical ASCII form (punycode for international names, canonical dotted IPv4) +// and returns an empty string when the host is not valid. +exports.tohost = function(host) { + + if (!host || typeof(host) !== 'string') + return ''; + + if (host.indexOf(':') === -1) + host = Url.domainToASCII(host); + + return exports.validhost(host) ? host : ''; +}; + +// Normalizes the hostname of a parsed URL (Url.parse() keeps "0127.0.0.1" or "0x7f.1" as they were written) +// Returns false when the hostname is not valid. +function normalizeuri(uri) { + + if (!uri.hostname) + return true; + + var hostname = exports.tohost(uri.hostname); + if (!hostname) + return false; + + if (hostname !== uri.hostname) { + var host = hostname + (uri.port ? (':' + uri.port) : ''); + if (uri.href) + uri.href = uri.href.replace(uri.host, host); + uri.hostname = hostname; + uri.host = host; + } + + return true; +} + exports.resolve = function(url, callback, param) { var uri; try { uri = Url.parse(url); + if (!normalizeuri(uri)) + throw new Error('Invalid hostname'); } catch (e) { callback(e); return; @@ -612,7 +663,7 @@ function _request(opt, callback) { var uri = opt.unixsocket ? { socketPath: opt.unixsocket.socket, path: opt.unixsocket.path } : Url.parse(opt.url); - if ((opt.unixsocket && !uri.socketPath) || (!opt.unixsocket && (!uri.hostname || !uri.host))) { + if ((opt.unixsocket && !uri.socketPath) || (!opt.unixsocket && (!uri.hostname || !uri.host || !normalizeuri(uri)))) { options.response.canceled = true; opt.callback && opt.callback('Invalid hostname', options.response); return; @@ -1019,6 +1070,32 @@ function request_response(res) { loc = uri.protocol + '//' + uri.hostname + (uri.port && !SKIPPORTS[uri.port] ? (':' + uri.port) : '') + loc; var tmp = Url.parse(loc); + + if (!normalizeuri(tmp)) { + options.timeoutid && clearTimeout(options.timeoutid); + options.canceled = true; + options.response.origin = options.origin; + options.response.headers = res.headers; + + if (options.callback) { + if (options.custom) { + options.response.status = res.statusCode; + options.response.stream = res; + } else { + options.response.status = 0; + options.response.host = uri.host; + } + options.callback('Invalid hostname', options.response); + options.callback = null; + } + + res.req.removeAllListeners(); + res.removeAllListeners(); + res.req = null; + res = null; + return; + } + tmp.headers = uri.headers; // Transfers cookies @@ -7065,6 +7142,15 @@ exports.connect = function(opt, callback) { delete opt.tls; + if (opt.host) { + opt.host = exports.tohost(opt.host); + if (!opt.host) { + meta.destroy = meta.close = NOOP; + callback && setImmediate(callback, new Error('Invalid hostname')); + return meta; + } + } + var close = function() { if (meta.socket1) { diff --git a/websocketclient.js b/websocketclient.js index b6dbc06..3649fd1 100644 --- a/websocketclient.js +++ b/websocketclient.js @@ -135,7 +135,13 @@ WebSocketClientProto.connectforce = function(self, url, protocol, origin) { if (typeof(url) === 'string') { url = Url.parse(url); - options.host = url.hostname; + options.host = framework_utils.tohost(url.hostname); + + if (!options.host) { + setImmediate(() => self.$events.error && self.emit('error', new Error('Invalid hostname'))); + return; + } + options.path = url.path; options.query = url.query; secured = url.protocol === 'wss:'; From 6a6e43f22cbd0ae4ffb25bcf01c1e0a160c2965c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gera=20G=2E=20G=C3=BCiles?= Date: Fri, 23 Oct 2026 21:36:06 +0200 Subject: [PATCH 2/2] Updated changelog with the Node.js October 2026 security hardening Documents the hostname normalization and validation (U.validhost, U.tohost, REQUEST, U.resolve, U.connect, WebSocket client, SMTP server) and the Node.js CVEs it covers (CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072). Notes that CVE-2026-105481 is fixed only by updating Node.js and that CVE-2026-111497 does not affect the framework. --- changelog.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog.txt b/changelog.txt index 9c853a8..a02ae98 100644 --- a/changelog.txt +++ b/changelog.txt @@ -9,6 +9,16 @@ - changed `max-age` header to `5 minutes` - fixed `attachmentfs()` in the mail: the file of the storage is read as a Buffer and the message is completed (it threw a `TypeError` and the SMTP connection stayed open) - added `QueryBuilder.autoqueryjson($, column, schema, [defsort], [maxlimit])` that works like `autoquery()` but for properties stored in a JSON column; the builder uses the neutral name `json:column:property:type` and the database adapter translates it into SQL +- security: hardened the hostnames against the Node.js security release of 2026-10-21 (patched in Node.js 22.23.4, 24.22.1 and 26.12.1) ; the framework does not require a specific version of Node.js, the hostnames are normalized and validated in the framework itself + - CVE-2026-105482: `dns.resolve*()` truncates a name at a NUL byte (`attacker.example\0.trusted.example`) + - CVE-2026-105483, CVE-2026-106069, CVE-2026-106072: `tls.checkServerIdentity()` with IPv4 literals written with non-ASCII digits, leading zeros or in a non-canonical form (`0x7f.0.0.1`, `127.1`, `2130706433`) +- added `U.validhost(host)` that accepts only plain hostnames and canonical IP literals +- added `U.tohost(host)` that converts a hostname to the canonical ASCII form (punycode, dotted IPv4) and returns an empty string for an invalid hostname +- fixed `REQUEST()`, `U.resolve()` and the redirects: the hostname of the URL is normalized (`Url.parse()` keeps `0127.0.0.1` or `0x7f.1` as written) and an invalid hostname gives the `Invalid hostname` error +- fixed `U.connect()` (used also by `LDAP()`): the `host` is normalized and an invalid hostname gives the `Invalid hostname` error +- fixed the WebSocket client: the hostname is normalized and an invalid hostname emits the `error` event +- fixed the SMTP server in the mail: the hostname is normalized (international names to punycode, IPv4 to the canonical form) and an invalid hostname gives the `Invalid SMTP server hostname.` error +- note: the issue CVE-2026-105481 (unsolicited data on a reused keep-alive socket, `http.Agent`) is fixed only by updating Node.js ; the Brotli issue CVE-2026-111497 does not affect the framework (only `gzip` and `deflate` are decompressed) ======================== 0.0.99