Skip to content

Commit e2d26c3

Browse files
committed
url: improve port validation
If a port is not a number, throw rather than treating the `:` that delineates the port as part of the path. This is consistent with WHATWG URL and also mitigates hostname-spoofing. Concerns about hostname-spoofing were raised and presented in excellent detail by pyozzi-toss (pyozzi@toss.im/Security-Tech Team in Toss).
1 parent 6fb466b commit e2d26c3

File tree

3 files changed

+21
-20
lines changed

3 files changed

+21
-20
lines changed

lib/url.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
377377
}
378378

379379
// pull out port.
380-
this.parseHost();
380+
this.parseHost(url);
381381

382382
// We've indicated that there is a hostname,
383383
// so even if it's empty, it has to be present.
@@ -392,7 +392,7 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
392392

393393
// validate a little.
394394
if (!ipv6Hostname) {
395-
rest = getHostname(this, rest, hostname);
395+
rest = getHostname(this, rest, hostname, url);
396396
}
397397

398398
if (this.hostname.length > hostnameMaxLen) {
@@ -511,7 +511,7 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
511511
return this;
512512
};
513513

514-
function getHostname(self, rest, hostname) {
514+
function getHostname(self, rest, hostname, url) {
515515
for (let i = 0; i < hostname.length; ++i) {
516516
const code = hostname.charCodeAt(i);
517517
const isValid = (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) ||
@@ -526,7 +526,12 @@ function getHostname(self, rest, hostname) {
526526
// Invalid host character
527527
if (!isValid) {
528528
self.hostname = hostname.slice(0, i);
529-
return `/${hostname.slice(i)}${rest}`;
529+
const leftover = hostname.slice(i);
530+
// If leftover starts with :, then it represents an invalid port.
531+
if (leftover.startsWith(':')) {
532+
throw new ERR_INVALID_URL(url);
533+
}
534+
return `/${leftover}${rest}`;
530535
}
531536
}
532537
return rest;

test/parallel/test-url-parse-format.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -865,22 +865,6 @@ const parseTests = {
865865
href: 'http://a%0D%22%20%09%0A%3C\'b:b@c/%0D%0Ad/e?f'
866866
},
867867

868-
// Git urls used by npm
869-
'git+ssh://git@github.com:npm/npm': {
870-
protocol: 'git+ssh:',
871-
slashes: true,
872-
auth: 'git',
873-
host: 'github.com',
874-
port: null,
875-
hostname: 'github.com',
876-
hash: null,
877-
search: null,
878-
query: null,
879-
pathname: '/:npm/npm',
880-
path: '/:npm/npm',
881-
href: 'git+ssh://git@github.com/:npm/npm'
882-
},
883-
884868
'https://*': {
885869
protocol: 'https:',
886870
slashes: true,

test/parallel/test-url-parse-invalid-input.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,15 @@ if (common.hasIntl) {
7474
(e) => e.code === 'ERR_INVALID_URL',
7575
'parsing http://\u00AD/bad.com/');
7676
}
77+
78+
{
79+
const badURLs = [
80+
'https://evil.com:.example.com',
81+
'git+ssh://git@github.com:npm/npm',
82+
];
83+
badURLs.forEach((badURL) => {
84+
assert.throws(() => { url.parse(badURL); },
85+
(e) => e.code === 'ERR_INVALID_URL',
86+
`parsing ${badURL}`);
87+
});
88+
}

0 commit comments

Comments
 (0)