Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
88 changes: 87 additions & 1 deletion utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion websocketclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:';
Expand Down
Loading