From af84da066add6daf843410bf0540a5620ac10b2b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 13 May 2021 20:50:33 +0200 Subject: [PATCH 01/12] [test] Fix multiple mixed slashes test Refs: https://github.com/unshiftio/url-parse/pull/197#discussion_r577898939 --- test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 216891e..eec3937 100644 --- a/test/test.js +++ b/test/test.js @@ -291,7 +291,7 @@ describe('url-parse', function () { assume(parsed.hostname).equals('github.com'); assume(parsed.pathname).equals('/foo/bar'); - url = 'https:/\/\/\github.com/foo/bar'; + url = 'https:/\\/\\/\\github.com/foo/bar'; assume(parsed.host).equals('github.com'); assume(parsed.hostname).equals('github.com'); assume(parsed.pathname).equals('/foo/bar'); From 9f43f43de91febafeb8c04985f494691c9925610 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 15 May 2021 08:59:05 +0200 Subject: [PATCH 02/12] [pkg] Update browserify to version 17.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f84b62e..ae11801 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "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", From d2979b586d8c7751e0c77f127d9ce1b2143cc0c9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 18 May 2021 07:00:54 +0200 Subject: [PATCH 03/12] [fix] Special case the `file:` protocol (#204) Fixes #203 --- index.js | 13 +++++++++---- test/test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 72b27c0..94e357e 100644 --- a/index.js +++ b/index.js @@ -33,7 +33,7 @@ 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('\\', '/'); + return address.replace(/\\/g, '/'); }, ['/', 'pathname'], // Extract from the back. ['@', 'auth', 1], // Extract from the front. @@ -224,7 +224,9 @@ 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.slashes || url.protocol === 'file:') { + instructions[3] = [/(.*)/, 'pathname']; + } for (; i < instructions.length; i++) { instruction = instructions[i]; @@ -288,7 +290,10 @@ 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) !== '/' + && (url.hostname || url.protocol === 'file:') + ) { url.pathname = '/' + url.pathname; } @@ -430,7 +435,7 @@ function toString(stringify) { if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':'; - var result = protocol + (url.slashes ? '//' : ''); + var result = protocol + (url.slashes || url.protocol === 'file:' ? '//' : ''); if (url.username) { result += url.username; diff --git a/test/test.js b/test/test.js index eec3937..38290ed 100644 --- a/test/test.js +++ b/test/test.js @@ -438,6 +438,48 @@ 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:///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('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 () { From ee22050a48a67409aa5f7c87947284156d615bd1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 3 Jun 2021 12:14:05 +0200 Subject: [PATCH 04/12] [ci] Use GitHub Actions --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ .travis.yml | 25 ------------------------- README.md | 2 +- package.json | 3 +-- test/browser.js | 2 +- 5 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..945a447 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + - push + - pull_request + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node: + - 12 + - 14 + - 16 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm test + - uses: coverallsapp/github-action@v1.1.2 + if: matrix.node == 12 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + test-browser: + runs-on: ubuntu-latest + env: + SAUCE_USERNAME: url-parse + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - run: npm install + - run: npm run test-browser diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7333c3e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: node_js -matrix: - fast_finish: true - include: - - node_js: "14" - env: SCRIPT=test - - node_js: "12" - env: SCRIPT=test - - node_js: "10" - env: SCRIPT=test - - node_js: "12" - env: - - secure: IF01oyIKSs0C5dARdYRTilKnU1TG4zenjjEPClkQxAWIpUOxl9xcNJWDVEOPxJ/4pVt+pozyT80Rp7efh6ZiREJIQI1tUboBKSqZzSbnD5uViQNSbQ90PaDP0FIUc0IQ5o07W36rijBB0DTmtU1VofzN9PKkJO7XiSSXevI8RcM= - - SAUCE_USERNAME=url-parse - - SCRIPT=test-browser -script: - - "npm run ${SCRIPT}" -after_script: - - 'if [ "${SCRIPT}" == "test" ]; then c8 report --reporter=text-lcov | coveralls; fi' -notifications: - irc: - channels: - - "irc.freenode.org#unshift" - on_success: change - on_failure: change 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/package.json b/package.json index ae11801..93a6797 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "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" @@ -40,7 +40,6 @@ "assume": "^2.2.0", "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) => { From 81ab967889b08112d3356e451bf03e6aa0cbb7e0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Jul 2021 18:31:42 +0200 Subject: [PATCH 05/12] [fix] Ignore slashes after the protocol for special URLs Fixes #205 Fixes #206 --- index.js | 51 ++++++++++++++++++++++++++++++++++------ test/test.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 94e357e..f424acc 100644 --- a/index.js +++ b/index.js @@ -98,6 +98,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,16 +128,32 @@ 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) - , protocol = match[1] ? match[1].toLowerCase() : '' - , slashes = !!(match[2] && match[2].length >= 2) - , rest = match[2] && match[2].length === 1 ? '/' + match[3] : match[3]; + var match = protocolre.exec(address); + var protocol = match[1] ? match[1].toLowerCase() : ''; + var rest = match[2] ? match[2] + match[3] : match[3]; + var slashes = !!(match[2] && match[2].length >= 2); + + if (protocol === 'file:') { + if (slashes) { + rest = rest.slice(2); + } + } else if (isSpecial(protocol)) { + rest = match[3]; + } else if (protocol) { + if (rest.indexOf('//') === 0) { + rest = rest.slice(2); + } + } else if (slashes && location.hostname) { + rest = match[3]; + } return { protocol: protocol, @@ -214,7 +248,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,7 +258,10 @@ function Url(address, location, parser) { // When the authority component is absent the URL starts with a path // component. // - if (!extracted.slashes || url.protocol === 'file:') { + if ( + url.protocol === 'file:' || + (!extracted.slashes && !isSpecial(extracted.protocol)) + ) { instructions[3] = [/(.*)/, 'pathname']; } diff --git a/test/test.js b/test/test.js index 38290ed..9a84fba 100644 --- a/test/test.js +++ b/test/test.js @@ -93,7 +93,7 @@ describe('url-parse', function () { assume(parse.extractProtocol('//foo/bar')).eql({ slashes: true, protocol: '', - rest: 'foo/bar' + rest: '//foo/bar' }); }); @@ -283,7 +283,7 @@ describe('url-parse', function () { 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); @@ -292,11 +292,59 @@ describe('url-parse', function () { assume(parsed.pathname).equals('/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'); + + url = 'https:/github.com/foo/bar'; + parsed = parse(url); + assume(parsed.host).equals('github.com'); + assume(parsed.pathname).equals('/foo/bar'); + + url = 'https:\\github.com/foo/bar'; + parsed = parse(url); + assume(parsed.host).equals('github.com'); + assume(parsed.pathname).equals('/foo/bar'); + + url = 'https:github.com/foo/bar'; + parsed = parse(url); + assume(parsed.host).equals('github.com'); + assume(parsed.pathname).equals('/foo/bar'); + + url = 'https:github.com/foo/bar'; + parsed = parse(url); + assume(parsed.host).equals('github.com'); + assume(parsed.pathname).equals('/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'); + + url = 'foo:/example.com'; + parsed = parse(url); + assume(parsed.hostname).equals(''); + assume(parsed.pathname).equals('/example.com'); + assume(parsed.href).equals('foo:/example.com'); + + url = 'foo://example.com'; + parsed = parse(url); + assume(parsed.hostname).equals('example.com'); + assume(parsed.pathname).equals('/'); + assume(parsed.href).equals('foo://example.com/'); + + url = 'foo:///example.com'; + parsed = parse(url); + assume(parsed.hostname).equals(''); + assume(parsed.pathname).equals('/example.com'); + assume(parsed.href).equals('foo:///example.com'); + }) + describe('origin', function () { it('generates an origin property', function () { var url = 'http://google.com:80/pathname' @@ -440,7 +488,7 @@ describe('url-parse', function () { }); it('handles the file: protocol', function () { - var slashes = ['', '/', '//', '///', '////', '/////']; + var slashes = ['', '/', '//', '///']; var data; var url; @@ -451,6 +499,18 @@ describe('url-parse', function () { 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:'); From 94872e7ab9103ee69b958959baa14c9e682a7f10 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 24 Jul 2021 09:27:05 +0200 Subject: [PATCH 06/12] [fix] Do not incorrectly set the `slashes` property to `true` Set it to `true` only if the protocol is special or if it is actually followed by two forward slashes. --- index.js | 44 ++++++++++++++++++++++++++++++++------------ test/test.js | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index f424acc..73b53f6 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,8 @@ 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 , 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 +'+'); @@ -138,26 +138,46 @@ function extractProtocol(address, location) { var match = protocolre.exec(address); var protocol = match[1] ? match[1].toLowerCase() : ''; - var rest = match[2] ? match[2] + match[3] : match[3]; - var slashes = !!(match[2] && match[2].length >= 2); + 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] + } + } if (protocol === 'file:') { - if (slashes) { + if (slashesCount >= 2) { rest = rest.slice(2); } } else if (isSpecial(protocol)) { - rest = match[3]; + rest = match[4]; } else if (protocol) { - if (rest.indexOf('//') === 0) { + if (forwardSlashes) { rest = rest.slice(2); } - } else if (slashes && location.hostname) { - rest = match[3]; + } else if (slashesCount >= 2 && location.hostname) { + rest = match[4]; } return { protocol: protocol, - slashes: slashes, + slashes: forwardSlashes || isSpecial(protocol), + slashesCount: slashesCount, rest: rest }; } @@ -260,7 +280,7 @@ function Url(address, location, parser) { // if ( url.protocol === 'file:' || - (!extracted.slashes && !isSpecial(extracted.protocol)) + (extracted.slashesCount < 2 && !isSpecial(extracted.protocol)) ) { instructions[3] = [/(.*)/, 'pathname']; } @@ -472,7 +492,7 @@ function toString(stringify) { if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':'; - var result = protocol + (url.slashes || url.protocol === 'file:' ? '//' : ''); + var result = protocol + (url.slashes || isSpecial(url.protocol) ? '//' : ''); if (url.username) { result += url.username; diff --git a/test/test.js b/test/test.js index 9a84fba..71cc473 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,6 +287,12 @@ 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('ignores slashes after the protocol for special URLs', function () { @@ -290,32 +302,44 @@ describe('url-parse', function () { 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.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 () { @@ -325,24 +349,28 @@ describe('url-parse', function () { 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.true(); }) describe('origin', function () { From fed6d9e338ea39de2d68bb66607066d71328c62f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 24 Jul 2021 09:35:58 +0200 Subject: [PATCH 07/12] [fix] Add a leading slash only if the URL is special If the value of the `pathname` property does not start with a `/`, add it only if the URL is special. --- index.js | 5 +---- test/test.js | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 73b53f6..6f2299a 100644 --- a/index.js +++ b/index.js @@ -347,10 +347,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 || url.protocol === 'file:') - ) { + if (url.pathname.charAt(0) !== '/' && isSpecial(url.protocol)) { url.pathname = '/' + url.pathname; } diff --git a/test/test.js b/test/test.js index 71cc473..fc240fc 100644 --- a/test/test.js +++ b/test/test.js @@ -361,8 +361,8 @@ describe('url-parse', function () { 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.pathname).equals(''); + assume(parsed.href).equals('foo://example.com'); assume(parsed.slashes).is.true(); url = 'foo:///example.com'; From fb128af4f43fa17f351d50cf615c7598c751f50a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 24 Jul 2021 18:19:10 +0200 Subject: [PATCH 08/12] [fix] Use `'null'` as `origin` for non special URLs --- index.js | 4 ++-- test/test.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 6f2299a..8e31ae3 100644 --- a/index.js +++ b/index.js @@ -371,7 +371,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'; @@ -464,7 +464,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'; diff --git a/test/test.js b/test/test.js index fc240fc..d5a6cab 100644 --- a/test/test.js +++ b/test/test.js @@ -395,6 +395,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'); From 2d9ac2c94067742b2116332c1e03be9f37371dff Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 25 Jul 2021 14:36:29 +0200 Subject: [PATCH 09/12] [fix] Sanitize only special URLs (#209) Fixes https://github.com/unshiftio/url-parse/pull/208#discussion_r675788224. --- index.js | 13 ++++++++----- test/test.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 8e31ae3..903fce2 100644 --- a/index.js +++ b/index.js @@ -32,8 +32,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(/\\/g, '/'); + 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. @@ -170,7 +170,7 @@ function extractProtocol(address, location) { if (forwardSlashes) { rest = rest.slice(2); } - } else if (slashesCount >= 2 && location.hostname) { + } else if (slashesCount >= 2 && isSpecial(location.protocol)) { rest = match[4]; } @@ -280,7 +280,10 @@ function Url(address, location, parser) { // if ( url.protocol === 'file:' || - (extracted.slashesCount < 2 && !isSpecial(extracted.protocol)) + (!extracted.slashes && + (extracted.protocol || + extracted.slashesCount < 2 || + !isSpecial(url.protocol))) ) { instructions[3] = [/(.*)/, 'pathname']; } @@ -289,7 +292,7 @@ function Url(address, location, parser) { instruction = instructions[i]; if (typeof instruction === 'function') { - address = instruction(address); + address = instruction(address, url); continue; } diff --git a/test/test.js b/test/test.js index d5a6cab..1893891 100644 --- a/test/test.js +++ b/test/test.js @@ -358,6 +358,13 @@ describe('url-parse', function () { 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'); @@ -365,13 +372,34 @@ describe('url-parse', function () { 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 () { it('generates an origin property', function () { From 201034b8670c2aa382d7ec410ee750ac6f2f9c38 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Sun, 25 Jul 2021 14:43:14 +0200 Subject: [PATCH 10/12] [dist] 1.5.2 --- SECURITY.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) 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/package.json b/package.json index 93a6797..3183f73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "url-parse", - "version": "1.5.1", + "version": "1.5.2", "description": "Small footprint URL parser that works seamlessly across Node.js and browser environments", "main": "index.js", "scripts": { From c7984617e235892cc22e0f47bb5ff1c012e6e39f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 26 Jul 2021 00:16:21 +0200 Subject: [PATCH 11/12] [fix] Fix host parsing for file URLs (#210) Fixes: https://github.com/unshiftio/url-parse/pull/209#issuecomment-886235848 --- index.js | 4 +++- test/test.js | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 903fce2..c6052d5 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ var required = require('requires-port') , qs = require('querystringify') , 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 +'+'); @@ -279,7 +280,8 @@ function Url(address, location, parser) { // component. // if ( - url.protocol === 'file:' || + extracted.protocol === 'file:' && ( + extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) || (!extracted.slashes && (extracted.protocol || extracted.slashesCount < 2 || diff --git a/test/test.js b/test/test.js index 1893891..8b34f7a 100644 --- a/test/test.js +++ b/test/test.js @@ -593,6 +593,13 @@ describe('url-parse', function () { 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'); From ad444931666a30bad11472d89a216461cf16cae2 Mon Sep 17 00:00:00 2001 From: Arnout Kazemier Date: Mon, 26 Jul 2021 00:17:19 +0200 Subject: [PATCH 12/12] [dist] 1.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3183f73..1364b9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "url-parse", - "version": "1.5.2", + "version": "1.5.3", "description": "Small footprint URL parser that works seamlessly across Node.js and browser environments", "main": "index.js", "scripts": {