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 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:';