[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#991737: unblock: node-url-parse/1.5.3-1



Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock

Please unblock package node-url-parse

[ Reason ]
node-url-parse 1.5.1 is vulnerable to URL redirection to untrusted
sites.

[ Impact ]
Medium security issue

[ Tests ]
Test passed (both build & autopkgtest)

[ Risks ]
Low risk: node-url-parse is a reverse dependency of:
 * node-miragejs (Build only)
 * node-original
   * node-eventsource

I tested rebuild & autopkgtest with success:
  rebuild      node-miragejs ... PASS
  autopkgtest  node-original ... PASS
  rebuild      node-original ... PASS

[ Checklist ]
  [X] all changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in testing

[ Other info ]
I prefered to update node-url-parse instead of backporting changes since
all changes are related to this vulnerabilities (including test updates)

You will find 2 debdiff:
 * full debdiff
 * relevant debdiff (only index.js changes)

Cheers,
Yadd

unblock node-url-parse/1.5.3-1
diff --git a/index.js b/index.js
index 72b27c0..c6052d5 100644
--- a/index.js
+++ b/index.js
@@ -2,8 +2,9 @@
 
 var required = require('requires-port')
   , qs = require('querystringify')
-  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:[\\/]+/
-  , protocolre = /^([a-z][a-z0-9.+-]*:)?([\\/]{1,})?([\S\s]*)/i
+  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//
+  , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i
+  , windowsDriveLetter = /^[a-zA-Z]:/
   , whitespace = '[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]'
   , left = new RegExp('^'+ whitespace +'+');
 
@@ -32,8 +33,8 @@ function trimLeft(str) {
 var rules = [
   ['#', 'hash'],                        // Extract from the back.
   ['?', 'query'],                       // Extract from the back.
-  function sanitize(address) {          // Sanitize what is left of the address
-    return address.replace('\\', '/');
+  function sanitize(address, url) {     // Sanitize what is left of the address
+    return isSpecial(url.protocol) ? address.replace(/\\/g, '/') : address;
   },
   ['/', 'pathname'],                    // Extract from the back.
   ['@', 'auth', 1],                     // Extract from the front.
@@ -98,6 +99,24 @@ function lolcation(loc) {
   return finaldestination;
 }
 
+/**
+ * Check whether a protocol scheme is special.
+ *
+ * @param {String} The protocol scheme of the URL
+ * @return {Boolean} `true` if the protocol scheme is special, else `false`
+ * @private
+ */
+function isSpecial(scheme) {
+  return (
+    scheme === 'file:' ||
+    scheme === 'ftp:' ||
+    scheme === 'http:' ||
+    scheme === 'https:' ||
+    scheme === 'ws:' ||
+    scheme === 'wss:'
+  );
+}
+
 /**
  * @typedef ProtocolExtract
  * @type Object
@@ -110,20 +129,56 @@ function lolcation(loc) {
  * Extract protocol information from a URL with/without double slash ("//").
  *
  * @param {String} address URL we want to extract from.
+ * @param {Object} location
  * @return {ProtocolExtract} Extracted information.
  * @private
  */
-function extractProtocol(address) {
+function extractProtocol(address, location) {
   address = trimLeft(address);
+  location = location || {};
+
+  var match = protocolre.exec(address);
+  var protocol = match[1] ? match[1].toLowerCase() : '';
+  var forwardSlashes = !!match[2];
+  var otherSlashes = !!match[3];
+  var slashesCount = 0;
+  var rest;
+
+  if (forwardSlashes) {
+    if (otherSlashes) {
+      rest = match[2] + match[3] + match[4];
+      slashesCount = match[2].length + match[3].length;
+    } else {
+      rest = match[2] + match[4];
+      slashesCount = match[2].length;
+    }
+  } else {
+    if (otherSlashes) {
+      rest = match[3] + match[4];
+      slashesCount = match[3].length;
+    } else {
+      rest = match[4]
+    }
+  }
 
-  var match = protocolre.exec(address)
-    , protocol = match[1] ? match[1].toLowerCase() : ''
-    , slashes = !!(match[2] && match[2].length >= 2)
-    , rest =  match[2] && match[2].length === 1 ? '/' + match[3] : match[3];
+  if (protocol === 'file:') {
+    if (slashesCount >= 2) {
+      rest = rest.slice(2);
+    }
+  } else if (isSpecial(protocol)) {
+    rest = match[4];
+  } else if (protocol) {
+    if (forwardSlashes) {
+      rest = rest.slice(2);
+    }
+  } else if (slashesCount >= 2 && isSpecial(location.protocol)) {
+    rest = match[4];
+  }
 
   return {
     protocol: protocol,
-    slashes: slashes,
+    slashes: forwardSlashes || isSpecial(protocol),
+    slashesCount: slashesCount,
     rest: rest
   };
 }
@@ -214,7 +269,7 @@ function Url(address, location, parser) {
   //
   // Extract protocol information before running the instructions.
   //
-  extracted = extractProtocol(address || '');
+  extracted = extractProtocol(address || '', location);
   relative = !extracted.protocol && !extracted.slashes;
   url.slashes = extracted.slashes || relative && location.slashes;
   url.protocol = extracted.protocol || location.protocol || '';
@@ -224,13 +279,22 @@ function Url(address, location, parser) {
   // When the authority component is absent the URL starts with a path
   // component.
   //
-  if (!extracted.slashes) instructions[3] = [/(.*)/, 'pathname'];
+  if (
+    extracted.protocol === 'file:' && (
+      extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) ||
+    (!extracted.slashes &&
+      (extracted.protocol ||
+        extracted.slashesCount < 2 ||
+        !isSpecial(url.protocol)))
+  ) {
+    instructions[3] = [/(.*)/, 'pathname'];
+  }
 
   for (; i < instructions.length; i++) {
     instruction = instructions[i];
 
     if (typeof instruction === 'function') {
-      address = instruction(address);
+      address = instruction(address, url);
       continue;
     }
 
@@ -288,7 +352,7 @@ function Url(address, location, parser) {
   // Default to a / for pathname if none exists. This normalizes the URL
   // to always have a /
   //
-  if (url.pathname.charAt(0) !== '/' && url.hostname) {
+  if (url.pathname.charAt(0) !== '/' && isSpecial(url.protocol)) {
     url.pathname = '/' + url.pathname;
   }
 
@@ -312,7 +376,7 @@ function Url(address, location, parser) {
     url.password = instruction[1] || '';
   }
 
-  url.origin = url.protocol && url.host && url.protocol !== 'file:'
+  url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host
     ? url.protocol +'//'+ url.host
     : 'null';
 
@@ -405,7 +469,7 @@ function set(part, value, fn) {
     if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase();
   }
 
-  url.origin = url.protocol && url.host && url.protocol !== 'file:'
+  url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host
     ? url.protocol +'//'+ url.host
     : 'null';
 
@@ -430,7 +494,7 @@ function toString(stringify) {
 
   if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':';
 
-  var result = protocol + (url.slashes ? '//' : '');
+  var result = protocol + (url.slashes || isSpecial(url.protocol) ? '//' : '');
 
   if (url.username) {
     result += url.username;
diff --git a/README.md b/README.md
index f81f919..4540e4b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # url-parse
 
-[![Made by unshift](https://img.shields.io/badge/made%20by-unshift-00ffcc.svg?style=flat-square)](http://unshift.io)[![Version npm](https://img.shields.io/npm/v/url-parse.svg?style=flat-square)](https://www.npmjs.com/package/url-parse)[![Build Status](https://img.shields.io/travis/unshiftio/url-parse/master.svg?style=flat-square)](https://travis-ci.org/unshiftio/url-parse)[![Dependencies](https://img.shields.io/david/unshiftio/url-parse.svg?style=flat-square)](https://david-dm.org/unshiftio/url-parse)[![Coverage Status](https://img.shields.io/coveralls/unshiftio/url-parse/master.svg?style=flat-square)](https://coveralls.io/r/unshiftio/url-parse?branch=master)[![IRC channel](https://img.shields.io/badge/IRC-irc.freenode.net%23unshift-00a8ff.svg?style=flat-square)](https://webchat.freenode.net/?channels=unshift)
+[![Made by unshift](https://img.shields.io/badge/made%20by-unshift-00ffcc.svg?style=flat-square)](http://unshift.io)[![Version npm](https://img.shields.io/npm/v/url-parse.svg?style=flat-square)](https://www.npmjs.com/package/url-parse)[![Build Status](https://img.shields.io/github/workflow/status/unshiftio/url-parse/CI/master?label=CI&style=flat-square)](https://github.com/unshiftio/url-parse/actions?query=workflow%3ACI+branch%3Amaster)[![Dependencies](https://img.shields.io/david/unshiftio/url-parse.svg?style=flat-square)](https://david-dm.org/unshiftio/url-parse)[![Coverage Status](https://img.shields.io/coveralls/unshiftio/url-parse/master.svg?style=flat-square)](https://coveralls.io/r/unshiftio/url-parse?branch=master)[![IRC channel](https://img.shields.io/badge/IRC-irc.freenode.net%23unshift-00a8ff.svg?style=flat-square)](https://webchat.freenode.net/?channels=unshift)
 
 [![Sauce Test Status](https://saucelabs.com/browser-matrix/url-parse.svg)](https://saucelabs.com/u/url-parse)
 
diff --git a/SECURITY.md b/SECURITY.md
index 31ef5b4..3a97067 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -33,6 +33,19 @@ acknowledge your responsible disclosure, if you wish.
 
 ## History
 
+> url-parse mishandles certain use a single of (back) slash such as https:\ &
+> https:/ and > interprets the URI as a relative path. Browsers accept a single
+> backslash after the protocol, and treat it as a normal slash, while url-parse
+> sees it as a relative path.
+
+- **Reporter credits**
+  - Ready-Research
+  - GitHub: [@Ready-Reserach](https://github.com/ready-research)
+- Huntr report: https://www.huntr.dev/bounties/1625557993985-unshiftio/url-parse/
+- Fixed in: 1.5.2
+
+---
+
 > Using backslash in the protocol is valid in the browser, while url-parse
 > thinks it’s a relative path. An application that validates a url using
 > url-parse might pass a malicious link.
@@ -42,6 +55,8 @@ acknowledge your responsible disclosure, if you wish.
   - Twitter: [Yaniv Nizry](https://twitter.com/ynizry)
 - Fixed in: 1.5.0
 
+---
+
 > The `extractProtocol` method does not return the correct protocol when
 > provided with unsanitized content which could lead to false positives.
 
diff --git a/debian/changelog b/debian/changelog
index ef5bb31..175b525 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+node-url-parse (1.5.3-1) unstable; urgency=medium
+
+  * Team upload
+  * Fix GitHub tags regex
+  * New upstream version 1.5.3 (Closes: #991577)
+
+ -- Yadd <yadd@debian.org>  Sat, 31 Jul 2021 13:13:02 +0200
+
 node-url-parse (1.5.1-1) unstable; urgency=medium
 
   * Team upload
diff --git a/debian/watch b/debian/watch
index 5636554..b2635d2 100644
--- a/debian/watch
+++ b/debian/watch
@@ -2,7 +2,7 @@ version=4
 opts=\
 dversionmangle=auto,\
 filenamemangle=s/.*\/v?([\d\.-]+)\.tar\.gz/node-url-parse-$1.tar.gz/ \
- https://github.com/unshiftio/url-parse/tags .*/archive/v?([\d\.]+).tar.gz
+ https://github.com/unshiftio/url-parse/tags .*/archive/.*/v?([\d\.]+).tar.gz
 
 # It is not recommended use npmregistry. Please investigate more.
 # Take a look at https://wiki.debian.org/debian/watch/
diff --git a/index.js b/index.js
index 72b27c0..c6052d5 100644
--- a/index.js
+++ b/index.js
@@ -2,8 +2,9 @@
 
 var required = require('requires-port')
   , qs = require('querystringify')
-  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:[\\/]+/
-  , protocolre = /^([a-z][a-z0-9.+-]*:)?([\\/]{1,})?([\S\s]*)/i
+  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//
+  , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i
+  , windowsDriveLetter = /^[a-zA-Z]:/
   , whitespace = '[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]'
   , left = new RegExp('^'+ whitespace +'+');
 
@@ -32,8 +33,8 @@ function trimLeft(str) {
 var rules = [
   ['#', 'hash'],                        // Extract from the back.
   ['?', 'query'],                       // Extract from the back.
-  function sanitize(address) {          // Sanitize what is left of the address
-    return address.replace('\\', '/');
+  function sanitize(address, url) {     // Sanitize what is left of the address
+    return isSpecial(url.protocol) ? address.replace(/\\/g, '/') : address;
   },
   ['/', 'pathname'],                    // Extract from the back.
   ['@', 'auth', 1],                     // Extract from the front.
@@ -98,6 +99,24 @@ function lolcation(loc) {
   return finaldestination;
 }
 
+/**
+ * Check whether a protocol scheme is special.
+ *
+ * @param {String} The protocol scheme of the URL
+ * @return {Boolean} `true` if the protocol scheme is special, else `false`
+ * @private
+ */
+function isSpecial(scheme) {
+  return (
+    scheme === 'file:' ||
+    scheme === 'ftp:' ||
+    scheme === 'http:' ||
+    scheme === 'https:' ||
+    scheme === 'ws:' ||
+    scheme === 'wss:'
+  );
+}
+
 /**
  * @typedef ProtocolExtract
  * @type Object
@@ -110,20 +129,56 @@ function lolcation(loc) {
  * Extract protocol information from a URL with/without double slash ("//").
  *
  * @param {String} address URL we want to extract from.
+ * @param {Object} location
  * @return {ProtocolExtract} Extracted information.
  * @private
  */
-function extractProtocol(address) {
+function extractProtocol(address, location) {
   address = trimLeft(address);
+  location = location || {};
+
+  var match = protocolre.exec(address);
+  var protocol = match[1] ? match[1].toLowerCase() : '';
+  var forwardSlashes = !!match[2];
+  var otherSlashes = !!match[3];
+  var slashesCount = 0;
+  var rest;
+
+  if (forwardSlashes) {
+    if (otherSlashes) {
+      rest = match[2] + match[3] + match[4];
+      slashesCount = match[2].length + match[3].length;
+    } else {
+      rest = match[2] + match[4];
+      slashesCount = match[2].length;
+    }
+  } else {
+    if (otherSlashes) {
+      rest = match[3] + match[4];
+      slashesCount = match[3].length;
+    } else {
+      rest = match[4]
+    }
+  }
 
-  var match = protocolre.exec(address)
-    , protocol = match[1] ? match[1].toLowerCase() : ''
-    , slashes = !!(match[2] && match[2].length >= 2)
-    , rest =  match[2] && match[2].length === 1 ? '/' + match[3] : match[3];
+  if (protocol === 'file:') {
+    if (slashesCount >= 2) {
+      rest = rest.slice(2);
+    }
+  } else if (isSpecial(protocol)) {
+    rest = match[4];
+  } else if (protocol) {
+    if (forwardSlashes) {
+      rest = rest.slice(2);
+    }
+  } else if (slashesCount >= 2 && isSpecial(location.protocol)) {
+    rest = match[4];
+  }
 
   return {
     protocol: protocol,
-    slashes: slashes,
+    slashes: forwardSlashes || isSpecial(protocol),
+    slashesCount: slashesCount,
     rest: rest
   };
 }
@@ -214,7 +269,7 @@ function Url(address, location, parser) {
   //
   // Extract protocol information before running the instructions.
   //
-  extracted = extractProtocol(address || '');
+  extracted = extractProtocol(address || '', location);
   relative = !extracted.protocol && !extracted.slashes;
   url.slashes = extracted.slashes || relative && location.slashes;
   url.protocol = extracted.protocol || location.protocol || '';
@@ -224,13 +279,22 @@ function Url(address, location, parser) {
   // When the authority component is absent the URL starts with a path
   // component.
   //
-  if (!extracted.slashes) instructions[3] = [/(.*)/, 'pathname'];
+  if (
+    extracted.protocol === 'file:' && (
+      extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) ||
+    (!extracted.slashes &&
+      (extracted.protocol ||
+        extracted.slashesCount < 2 ||
+        !isSpecial(url.protocol)))
+  ) {
+    instructions[3] = [/(.*)/, 'pathname'];
+  }
 
   for (; i < instructions.length; i++) {
     instruction = instructions[i];
 
     if (typeof instruction === 'function') {
-      address = instruction(address);
+      address = instruction(address, url);
       continue;
     }
 
@@ -288,7 +352,7 @@ function Url(address, location, parser) {
   // Default to a / for pathname if none exists. This normalizes the URL
   // to always have a /
   //
-  if (url.pathname.charAt(0) !== '/' && url.hostname) {
+  if (url.pathname.charAt(0) !== '/' && isSpecial(url.protocol)) {
     url.pathname = '/' + url.pathname;
   }
 
@@ -312,7 +376,7 @@ function Url(address, location, parser) {
     url.password = instruction[1] || '';
   }
 
-  url.origin = url.protocol && url.host && url.protocol !== 'file:'
+  url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host
     ? url.protocol +'//'+ url.host
     : 'null';
 
@@ -405,7 +469,7 @@ function set(part, value, fn) {
     if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase();
   }
 
-  url.origin = url.protocol && url.host && url.protocol !== 'file:'
+  url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host
     ? url.protocol +'//'+ url.host
     : 'null';
 
@@ -430,7 +494,7 @@ function toString(stringify) {
 
   if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':';
 
-  var result = protocol + (url.slashes ? '//' : '');
+  var result = protocol + (url.slashes || isSpecial(url.protocol) ? '//' : '');
 
   if (url.username) {
     result += url.username;
diff --git a/package.json b/package.json
index f84b62e..1364b9b 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
 {
   "name": "url-parse",
-  "version": "1.5.1",
+  "version": "1.5.3",
   "description": "Small footprint URL parser that works seamlessly across Node.js and browser environments",
   "main": "index.js",
   "scripts": {
     "browserify": "rm -rf dist && mkdir -p dist && browserify index.js -s URLParse -o dist/url-parse.js",
     "minify": "uglifyjs dist/url-parse.js --source-map -cm -o dist/url-parse.min.js",
-    "test": "c8 --reporter=html --reporter=text mocha test/test.js",
+    "test": "c8 --reporter=lcov --reporter=text mocha test/test.js",
     "test-browser": "node test/browser.js",
     "prepublishOnly": "npm run browserify && npm run minify",
     "watch": "mocha --watch test/test.js"
@@ -38,9 +38,8 @@
   },
   "devDependencies": {
     "assume": "^2.2.0",
-    "browserify": "^16.2.3",
+    "browserify": "^17.0.0",
     "c8": "^7.3.1",
-    "coveralls": "^3.1.0",
     "mocha": "^8.0.1",
     "pre-commit": "^1.2.2",
     "sauce-browsers": "^2.0.0",
diff --git a/test/browser.js b/test/browser.js
index 200ec5e..63ee99b 100644
--- a/test/browser.js
+++ b/test/browser.js
@@ -29,12 +29,12 @@ const platforms = sauceBrowsers([
 });
 
 run(path.join(__dirname, 'test.js'), 'saucelabs', {
+  jobInfo: { name: pkg.name, build: process.env.GITHUB_RUN_ID },
   html: path.join(__dirname, 'index.html'),
   accessKey: process.env.SAUCE_ACCESS_KEY,
   username: process.env.SAUCE_USERNAME,
   browserify: true,
   disableSSL: true,
-  name: pkg.name,
   parallel: 5,
   platforms
 }).done((results) => {
diff --git a/test/test.js b/test/test.js
index 216891e..8b34f7a 100644
--- a/test/test.js
+++ b/test/test.js
@@ -71,7 +71,8 @@ describe('url-parse', function () {
       assume(parse.extractProtocol('http://example.com')).eql({
         slashes: true,
         protocol: 'http:',
-        rest: 'example.com'
+        rest: 'example.com',
+        slashesCount: 2
       });
     });
 
@@ -79,7 +80,8 @@ describe('url-parse', function () {
       assume(parse.extractProtocol('')).eql({
         slashes: false,
         protocol: '',
-        rest: ''
+        rest: '',
+        slashesCount: 0
       });
     });
 
@@ -87,13 +89,15 @@ describe('url-parse', function () {
       assume(parse.extractProtocol('/foo')).eql({
         slashes: false,
         protocol: '',
-        rest: '/foo'
+        rest: '/foo',
+        slashesCount: 1
       });
 
       assume(parse.extractProtocol('//foo/bar')).eql({
         slashes: true,
         protocol: '',
-        rest: 'foo/bar'
+        rest: '//foo/bar',
+        slashesCount: 2
       });
     });
 
@@ -103,7 +107,8 @@ describe('url-parse', function () {
       assume(parse.extractProtocol(input)).eql({
         slashes: false,
         protocol: '',
-        rest: input
+        rest: input,
+        slashesCount: 0
       });
     });
 
@@ -111,7 +116,8 @@ describe('url-parse', function () {
       assume(parse.extractProtocol(' javascript://foo')).eql({
         slashes: true,
         protocol: 'javascript:',
-        rest: 'foo'
+        rest: 'foo',
+        slashesCount: 2
       });
     });
   });
@@ -281,20 +287,118 @@ describe('url-parse', function () {
 
     assume(parsed.host).equals('what-is-up.com');
     assume(parsed.href).equals('http://what-is-up.com/');
+
+    url = '\\\\\\\\what-is-up.com'
+    parsed = parse(url, parse('http://google.com'));
+
+    assume(parsed.host).equals('what-is-up.com');
+    assume(parsed.href).equals('http://what-is-up.com/');
   });
 
-  it('does not see a slash after the protocol as path', function () {
+  it('ignores slashes after the protocol for special URLs', function () {
     var url = 'https:\\/github.com/foo/bar'
       , parsed = parse(url);
 
     assume(parsed.host).equals('github.com');
     assume(parsed.hostname).equals('github.com');
     assume(parsed.pathname).equals('/foo/bar');
+    assume(parsed.slashes).is.true();
+    assume(parsed.href).equals('https://github.com/foo/bar');
 
-    url = 'https:/\/\/\github.com/foo/bar';
+    url = 'https:/\\/\\/\\github.com/foo/bar';
+    parsed = parse(url);
     assume(parsed.host).equals('github.com');
     assume(parsed.hostname).equals('github.com');
     assume(parsed.pathname).equals('/foo/bar');
+    assume(parsed.slashes).is.true();
+    assume(parsed.href).equals('https://github.com/foo/bar');
+
+    url = 'https:/github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+    assume(parsed.slashes).is.true();
+    assume(parsed.href).equals('https://github.com/foo/bar');
+
+    url = 'https:\\github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+    assume(parsed.slashes).is.true();
+    assume(parsed.href).equals('https://github.com/foo/bar');
+
+    url = 'https:github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+    assume(parsed.slashes).is.true();
+    assume(parsed.href).equals('https://github.com/foo/bar');
+
+    url = 'https:github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+    assume(parsed.slashes).is.true();
+    assume(parsed.href).equals('https://github.com/foo/bar');
+  });
+
+  it('handles slashes after the protocol for non special URLs', function () {
+    var url = 'foo:example.com'
+      , parsed = parse(url);
+
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('example.com');
+    assume(parsed.href).equals('foo:example.com');
+    assume(parsed.slashes).is.false();
+
+    url = 'foo:/example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('/example.com');
+    assume(parsed.href).equals('foo:/example.com');
+    assume(parsed.slashes).is.false();
+
+    url = 'foo:\\example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('\\example.com');
+    assume(parsed.href).equals('foo:\\example.com');
+    assume(parsed.slashes).is.false();
+
+    url = 'foo://example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('example.com');
+    assume(parsed.pathname).equals('');
+    assume(parsed.href).equals('foo://example.com');
+    assume(parsed.slashes).is.true();
+
+    url = 'foo:\\\\example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('\\\\example.com');
+    assume(parsed.href).equals('foo:\\\\example.com');
+    assume(parsed.slashes).is.false();
+
+    url = 'foo:///example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('/example.com');
+    assume(parsed.href).equals('foo:///example.com');
+    assume(parsed.slashes).is.true();
+
+    url = 'foo:\\\\\\example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('\\\\\\example.com');
+    assume(parsed.href).equals('foo:\\\\\\example.com');
+    assume(parsed.slashes).is.false();
+
+    url = '\\\\example.com/foo/bar';
+    parsed = parse(url, 'foo://bar.com');
+    assume(parsed.hostname).equals('bar.com');
+    assume(parsed.pathname).equals('/\\\\example.com/foo/bar');
+    assume(parsed.href).equals('foo://bar.com/\\\\example.com/foo/bar');
+    assume(parsed.slashes).is.true();
   });
 
   describe('origin', function () {
@@ -319,6 +423,13 @@ describe('url-parse', function () {
       assume(parsed.origin).equals('null');
     });
 
+    it('is null for non special URLs', function () {
+      var o = parse('foo://example.com/pathname');
+      assume(o.hostname).equals('example.com');
+      assume(o.pathname).equals('/pathname');
+      assume(o.origin).equals('null');
+    });
+
     it('removes default ports for http', function () {
       var o = parse('http://google.com:80/pathname');
       assume(o.origin).equals('http://google.com');
@@ -438,6 +549,67 @@ describe('url-parse', function () {
       data.set('protocol', 'https:');
       assume(data.href).equals('https://google.com/foo');
     });
+
+    it('handles the file: protocol', function () {
+      var slashes = ['', '/', '//', '///'];
+      var data;
+      var url;
+
+      for (var i = 0; i < slashes.length; i++) {
+        data = parse('file:' + slashes[i]);
+        assume(data.protocol).equals('file:');
+        assume(data.pathname).equals('/');
+        assume(data.href).equals('file:///');
+      }
+
+      url = 'file:////';
+      data = parse(url);
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('//');
+      assume(data.href).equals(url);
+
+      url = 'file://///';
+      data = parse(url);
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('///');
+      assume(data.href).equals(url);
+
+      url = 'file:///Users/foo/BAR/baz.pdf';
+      data = parse(url);
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('/Users/foo/BAR/baz.pdf');
+      assume(data.href).equals(url);
+
+      url = 'file:///foo/bar?baz=qux#hash';
+      data = parse(url);
+      assume(data.protocol).equals('file:');
+      assume(data.hash).equals('#hash');
+      assume(data.query).equals('?baz=qux');
+      assume(data.pathname).equals('/foo/bar');
+      assume(data.href).equals(url);
+
+      data = parse('file://c:\\foo\\bar\\');
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('/c:/foo/bar/');
+      assume(data.href).equals('file:///c:/foo/bar/');
+
+      data = parse('file://host/file');
+      assume(data.protocol).equals('file:');
+      assume(data.host).equals('host');
+      assume(data.hostname).equals('host');
+      assume(data.pathname).equals('/file');
+      assume(data.href).equals('file://host/file');
+
+      data = parse('foo/bar', 'file:///baz');
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('/foo/bar');
+      assume(data.href).equals('file:///foo/bar');
+
+      data = parse('foo/bar', 'file:///baz/');
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('/baz/foo/bar');
+      assume(data.href).equals('file:///baz/foo/bar');
+    });
   });
 
   describe('ip', function () {

Reply to: