From 8b1405ee014033d9a36873f65ca49be11f15a569 Mon Sep 17 00:00:00 2001
From: Matteo Collina <hello@matteocollina.com>
Date: Mon, 10 Sep 2018 12:57:07 +0200
Subject: [PATCH] url: avoid hostname spoofing w/ javascript protocol

CVE-2018-12123

Fixes: https://github.com/nodejs-private/security/issues/205
PR-URL: https://github.com/nodejs-private/node-private/pull/145
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 lib/url.js                             |  4 +-
 test/parallel/test-url-parse-format.js | 55 ++++++++++++++++++++++++++
 2 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/lib/url.js b/lib/url.js
index ba2033b4e53..676722de622 100644
--- a/lib/url.js
+++ b/lib/url.js
@@ -267,13 +267,13 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) {
   if (slashesDenoteHost || proto || hostPattern.test(rest)) {
     var slashes = rest.charCodeAt(0) === CHAR_FORWARD_SLASH &&
                   rest.charCodeAt(1) === CHAR_FORWARD_SLASH;
-    if (slashes && !(proto && hostlessProtocol[proto])) {
+    if (slashes && !(proto && hostlessProtocol[lowerProto])) {
       rest = rest.slice(2);
       this.slashes = true;
     }
   }
 
-  if (!hostlessProtocol[proto] &&
+  if (!hostlessProtocol[lowerProto] &&
       (slashes || (proto && !slashedProtocol[proto]))) {
 
     // there's a hostname.
diff --git a/test/parallel/test-url-parse-format.js b/test/parallel/test-url-parse-format.js
index f4e72ee5ef4..b18a5fe585d 100644
--- a/test/parallel/test-url-parse-format.js
+++ b/test/parallel/test-url-parse-format.js
@@ -890,6 +890,39 @@ const parseTests = {
     pathname: '/*',
     path: '/*',
     href: 'https:///*'
+  },
+
+  // The following two URLs are the same, but they differ for
+  // a capital A: it is important that we verify that the protocol
+  // is checked in a case-insensitive manner.
+  'javascript:alert(1);a=\x27@white-listed.com\x27': {
+    protocol: 'javascript:',
+    slashes: null,
+    auth: null,
+    host: null,
+    port: null,
+    hostname: null,
+    hash: null,
+    search: null,
+    query: null,
+    pathname: "alert(1);a='@white-listed.com'",
+    path: "alert(1);a='@white-listed.com'",
+    href: "javascript:alert(1);a='@white-listed.com'"
+  },
+
+  'javAscript:alert(1);a=\x27@white-listed.com\x27': {
+    protocol: 'javascript:',
+    slashes: null,
+    auth: null,
+    host: null,
+    port: null,
+    hostname: null,
+    hash: null,
+    search: null,
+    query: null,
+    pathname: "alert(1);a='@white-listed.com'",
+    path: "alert(1);a='@white-listed.com'",
+    href: "javascript:alert(1);a='@white-listed.com'"
   }
 };
 
@@ -921,3 +954,25 @@ for (const u in parseTests) {
   assert.strictEqual(actual, expected,
                      `format(${u}) == ${u}\nactual:${actual}`);
 }
+
+{
+  const parsed = url.parse('http://nodejs.org/')
+    .resolveObject('jAvascript:alert(1);a=\x27@white-listed.com\x27');
+
+  const expected = Object.assign(new url.Url(), {
+    protocol: 'javascript:',
+    slashes: null,
+    auth: null,
+    host: null,
+    port: null,
+    hostname: null,
+    hash: null,
+    search: null,
+    query: null,
+    pathname: "alert(1);a='@white-listed.com'",
+    path: "alert(1);a='@white-listed.com'",
+    href: "javascript:alert(1);a='@white-listed.com'"
+  });
+
+  assert.deepStrictEqual(parsed, expected);
+}
