url: runtime-deprecate url.parse() with invalid ports

These URLs throw with WHATWG URL. They are permitted with url.parse()
but that allows potential host spoofing by sending a domain name as the
port.

PR-URL: https://github.com/nodejs/node/pull/45526
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Rich Trott 2022-11-19 13:46:44 -08:00
parent 2589fb1f6b
commit 3bed5f11e0
3 changed files with 45 additions and 3 deletions

View file

@ -3302,13 +3302,17 @@ issued for `url.parse()` vulnerabilities.
<!-- YAML <!-- YAML
changes: changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/45526
description: Runtime deprecation.
- version: - version:
- v19.2.0 - v19.2.0
pr-url: https://github.com/nodejs/node/pull/45576 pr-url: https://github.com/nodejs/node/pull/45576
description: Documentation-only deprecation. description: Documentation-only deprecation.
--> -->
Type: Documentation-only Type: Runtime
[`url.parse()`][] accepts URLs with ports that are not numbers. This behavior [`url.parse()`][] accepts URLs with ports that are not numbers. This behavior
might result in host name spoofing with unexpected input. These URLs will throw might result in host name spoofing with unexpected input. These URLs will throw

View file

@ -387,7 +387,7 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
// validate a little. // validate a little.
if (!ipv6Hostname) { if (!ipv6Hostname) {
rest = getHostname(this, rest, hostname); rest = getHostname(this, rest, hostname, url);
} }
if (this.hostname.length > hostnameMaxLen) { if (this.hostname.length > hostnameMaxLen) {
@ -506,7 +506,8 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
return this; return this;
}; };
function getHostname(self, rest, hostname) { let warnInvalidPort = true;
function getHostname(self, rest, hostname, url) {
for (let i = 0; i < hostname.length; ++i) { for (let i = 0; i < hostname.length; ++i) {
const code = hostname.charCodeAt(i); const code = hostname.charCodeAt(i);
const isValid = (code !== CHAR_FORWARD_SLASH && const isValid = (code !== CHAR_FORWARD_SLASH &&
@ -516,6 +517,14 @@ function getHostname(self, rest, hostname) {
code !== CHAR_COLON); code !== CHAR_COLON);
if (!isValid) { if (!isValid) {
// If leftover starts with :, then it represents an invalid port.
// But url.parse() is lenient about it for now.
// Issue a warning and continue.
if (warnInvalidPort && code === CHAR_COLON) {
const detail = `The URL ${url} is invalid. Future versions of Node.js will throw an error.`;
process.emitWarning(detail, 'DeprecationWarning', 'DEP0170');
warnInvalidPort = false;
}
self.hostname = hostname.slice(0, i); self.hostname = hostname.slice(0, i);
return `/${hostname.slice(i)}${rest}`; return `/${hostname.slice(i)}${rest}`;
} }

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
const childProcess = require('child_process');
const url = require('url'); const url = require('url');
// https://github.com/joyent/node/issues/568 // https://github.com/joyent/node/issues/568
@ -74,3 +75,31 @@ if (common.hasIntl) {
(e) => e.code === 'ERR_INVALID_URL', (e) => e.code === 'ERR_INVALID_URL',
'parsing http://\u00AD/bad.com/'); 'parsing http://\u00AD/bad.com/');
} }
{
const badURLs = [
'https://evil.com:.example.com',
'git+ssh://git@github.com:npm/npm',
];
badURLs.forEach((badURL) => {
childProcess.exec(`${process.execPath} -e "url.parse('${badURL}')"`,
common.mustCall((err, stdout, stderr) => {
assert.strictEqual(err, null);
assert.strictEqual(stdout, '');
assert.match(stderr, /\[DEP0170\] DeprecationWarning:/);
})
);
});
// Warning should only happen once per process.
const expectedWarning = [
`The URL ${badURLs[0]} is invalid. Future versions of Node.js will throw an error.`,
'DEP0170',
];
common.expectWarning({
DeprecationWarning: expectedWarning,
});
badURLs.forEach((badURL) => {
url.parse(badURL);
});
}