Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: unshiftio/url-parse
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.5.4
Choose a base ref
...
head repository: unshiftio/url-parse
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.5.7
Choose a head ref
  • 13 commits
  • 5 files changed
  • 2 contributors

Commits on Jan 9, 2022

  1. Copy the full SHA
    f7774f6 View commit details

Commits on Jan 30, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9be7ee8 View commit details

Commits on Feb 11, 2022

  1. Copy the full SHA
    4e53a8c View commit details

Commits on Feb 13, 2022

  1. [fix] Remove CR, HT, and LF

    Copy the behavior of browser `URL` interface and remove CR, HT, and LF
    from the input URL.
    lpinca committed Feb 13, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    319851b View commit details
  2. Copy the full SHA
    193b44b View commit details
  3. 1.5.5

    Swaagie committed Feb 13, 2022
    Copy the full SHA
    e4a5807 View commit details
  4. Merge pull request #223 from unshiftio/fix/at-sign-handling-in-userinfo

    Correctly handle userinfo containing the at sign
    Swaagie authored Feb 13, 2022
    Copy the full SHA
    7b0b8a6 View commit details
  5. 1.5.6

    Swaagie committed Feb 13, 2022
    Copy the full SHA
    4c9fa23 View commit details
  6. Copy the full SHA
    e6fa434 View commit details
  7. [security] Fix nits

    lpinca committed Feb 13, 2022
    Copy the full SHA
    78e9f2f View commit details

Commits on Feb 16, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    88df234 View commit details
  2. [fix] Readd the empty userinfo to url.href (#226)

    If the userinfo is present but empty, the parsed host is also empty, and
    `url.pathname` is not `'/'`, then readd the empty userinfo to `url.href`,
    otherwise the original invalid URL might be transformed into a valid one
    with `url.pathname` as host.
    lpinca authored Feb 16, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ef45a13 View commit details
  3. 1.5.7

    Swaagie committed Feb 16, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8b3f5f2 View commit details
Showing with 232 additions and 19 deletions.
  1. +8 −2 README.md
  2. +14 −4 SECURITY.md
  3. +46 −10 index.js
  4. +1 −1 package.json
  5. +163 −2 test/test.js
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,12 @@

[![Sauce Test Status](https://saucelabs.com/browser-matrix/url-parse.svg)](https://saucelabs.com/u/url-parse)

**`url-parse` was created in 2014 when the WHATWG URL API was not available in
Node.js and the `URL` interface was supported only in some browsers. Today this
is no longer true. The `URL` interface is available in all supported Node.js
release lines and basically all browsers. Consider using it for better security
and accuracy.**

The `url-parse` method exposes two different API interfaces. The
[`url`](https://nodejs.org/api/url.html) interface that you know from Node.js
and the new [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL)
@@ -80,8 +86,8 @@ The returned `url` instance contains the following properties:
- `auth`: Authentication information portion (e.g. `username:password`).
- `username`: Username of basic authentication.
- `password`: Password of basic authentication.
- `host`: Host name with port number.
- `hostname`: Host name without port number.
- `host`: Host name with port number. The hostname might be invalid.
- `hostname`: Host name without port number. This might be an invalid hostname.
- `port`: Optional port number.
- `pathname`: URL path.
- `query`: Parsed object containing query string, unless parsing is set to false.
18 changes: 14 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -33,14 +33,24 @@ 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
> Incorrect handling of username and password can lead to authorization bypass.
- **Reporter credits**
- ranjit-git
- GitHub: [@ranjit-git](https://github.com/ranjit-git)
- Huntr report: https://www.huntr.dev/bounties/6d1bc51f-1876-4f5b-a2c2-734e09e8e05b/
- Fixed in: 1.5.6

---

> url-parse mishandles certain uses of a single (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)
- ready-research
- GitHub: [@ready-research](https://github.com/ready-research)
- Huntr report: https://www.huntr.dev/bounties/1625557993985-unshiftio/url-parse/
- Fixed in: 1.5.2

56 changes: 46 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -2,11 +2,11 @@

var required = require('requires-port')
, qs = require('querystringify')
, CRHTLF = /[\n\r\t]/g
, 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 +'+');
, whitespace = /^[ \f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/;

/**
* Trim a given string.
@@ -15,7 +15,7 @@ var required = require('requires-port')
* @public
*/
function trimLeft(str) {
return (str ? str : '').toString().replace(left, '');
return (str ? str : '').toString().replace(whitespace, '');
}

/**
@@ -135,6 +135,7 @@ function isSpecial(scheme) {
*/
function extractProtocol(address, location) {
address = trimLeft(address);
address = address.replace(CRHTLF, '');
location = location || {};

var match = protocolre.exec(address);
@@ -235,6 +236,7 @@ function resolve(relative, base) {
*/
function Url(address, location, parser) {
address = trimLeft(address);
address = address.replace(CRHTLF, '');

if (!(this instanceof Url)) {
return new Url(address, location, parser);
@@ -304,7 +306,11 @@ function Url(address, location, parser) {
if (parse !== parse) {
url[key] = address;
} else if ('string' === typeof parse) {
if (~(index = address.indexOf(parse))) {
index = parse === '@'
? address.lastIndexOf(parse)
: address.indexOf(parse);

if (~index) {
if ('number' === typeof instruction[2]) {
url[key] = address.slice(0, index);
address = address.slice(index + instruction[2]);
@@ -370,10 +376,21 @@ function Url(address, location, parser) {
// Parse down the `auth` for the username and password.
//
url.username = url.password = '';

if (url.auth) {
instruction = url.auth.split(':');
url.username = instruction[0];
url.password = instruction[1] || '';
index = url.auth.indexOf(':');

if (~index) {
url.username = url.auth.slice(0, index);
url.username = encodeURIComponent(decodeURIComponent(url.username));

url.password = url.auth.slice(index + 1);
url.password = encodeURIComponent(decodeURIComponent(url.password))
} else {
url.username = encodeURIComponent(decodeURIComponent(url.auth));
}

url.auth = url.password ? url.username +':'+ url.password : url.username;
}

url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host
@@ -465,9 +482,17 @@ function set(part, value, fn) {
break;

case 'auth':
var splits = value.split(':');
url.username = splits[0];
url.password = splits.length === 2 ? splits[1] : '';
var index = value.indexOf(':');

if (~index) {
url.username = value.slice(0, index);
url.username = encodeURIComponent(decodeURIComponent(url.username));

url.password = value.slice(index + 1);
url.password = encodeURIComponent(decodeURIComponent(url.password));
} else {
url.username = encodeURIComponent(decodeURIComponent(value));
}
}

for (var i = 0; i < rules.length; i++) {
@@ -514,6 +539,17 @@ function toString(stringify) {
} else if (url.password) {
result += ':'+ url.password;
result += '@';
} else if (
url.protocol !== 'file:' &&
isSpecial(url.protocol) &&
!url.host &&
url.pathname !== '/'
) {
//
// Add back the empty userinfo, otherwise the original invalid URL
// might be transformed into a valid one with `url.pathname` as host.
//
result += '@';
}

result += url.host + url.pathname;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "url-parse",
"version": "1.5.4",
"version": "1.5.7",
"description": "Small footprint URL parser that works seamlessly across Node.js and browser environments",
"main": "index.js",
"scripts": {
165 changes: 163 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -102,7 +102,7 @@ describe('url-parse', function () {
});

it('does not truncate the input string', function () {
var input = 'foo\nbar\rbaz\u2028qux\u2029';
var input = 'foo\x0bbar\x0cbaz\u2028qux\u2029';

assume(parse.extractProtocol(input)).eql({
slashes: false,
@@ -113,7 +113,16 @@ describe('url-parse', function () {
});

it('trimsLeft', function () {
assume(parse.extractProtocol(' javascript://foo')).eql({
assume(parse.extractProtocol('\x0b\x0c javascript://foo')).eql({
slashes: true,
protocol: 'javascript:',
rest: 'foo',
slashesCount: 2
});
});

it('removes CR, HT, and LF', function () {
assume(parse.extractProtocol('jav\n\rasc\nript\r:/\t/fo\no')).eql({
slashes: true,
protocol: 'javascript:',
rest: 'foo',
@@ -408,6 +417,31 @@ describe('url-parse', function () {
assume(parsed.href).equals('//example.com');
});

it('removes CR, HT, and LF', function () {
var parsed = parse(
'ht\ntp://a\rb:\tcd@exam\rple.com:80\t80/pat\thname?fo\no=b\rar#ba\tz'
);

assume(parsed.protocol).equals('http:');
assume(parsed.username).equals('ab');
assume(parsed.password).equals('cd');
assume(parsed.host).equals('example.com:8080');
assume(parsed.hostname).equals('example.com');
assume(parsed.port).equals('8080');
assume(parsed.pathname).equals('/pathname');
assume(parsed.query).equals('?foo=bar');
assume(parsed.hash).equals('#baz');
assume(parsed.href).equals(
'http://ab:cd@example.com:8080/pathname?foo=bar#baz'
);

parsed = parse('s\nip:al\rice@atl\tanta.com');

assume(parsed.protocol).equals('sip:');
assume(parsed.pathname).equals('alice@atlanta.com');
assume(parsed.href).equals('sip:alice@atlanta.com');
});

describe('origin', function () {
it('generates an origin property', function () {
var url = 'http://google.com:80/pathname'
@@ -689,6 +723,113 @@ describe('url-parse', function () {
assume(parsed.hostname).equals('www.example.com');
assume(parsed.href).equals(url);
});

it('handles @ in username', function () {
var url = 'http://user@@www.example.com/'
, parsed = parse(url);

assume(parsed.protocol).equals('http:');
assume(parsed.auth).equals('user%40');
assume(parsed.username).equals('user%40');
assume(parsed.password).equals('');
assume(parsed.hostname).equals('www.example.com');
assume(parsed.pathname).equals('/');
assume(parsed.href).equals('http://user%40@www.example.com/');

url = 'http://user%40@www.example.com/';
parsed = parse(url);

assume(parsed.protocol).equals('http:');
assume(parsed.auth).equals('user%40');
assume(parsed.username).equals('user%40');
assume(parsed.password).equals('');
assume(parsed.hostname).equals('www.example.com');
assume(parsed.pathname).equals('/');
assume(parsed.href).equals('http://user%40@www.example.com/');
});

it('handles @ in password', function () {
var url = 'http://user@:pas:s@@www.example.com/'
, parsed = parse(url);

assume(parsed.protocol).equals('http:');
assume(parsed.auth).equals('user%40:pas%3As%40');
assume(parsed.username).equals('user%40');
assume(parsed.password).equals('pas%3As%40');
assume(parsed.hostname).equals('www.example.com');
assume(parsed.pathname).equals('/');
assume(parsed.href).equals('http://user%40:pas%3As%40@www.example.com/');

url = 'http://user%40:pas%3As%40@www.example.com/'
parsed = parse(url);

assume(parsed.protocol).equals('http:');
assume(parsed.auth).equals('user%40:pas%3As%40');
assume(parsed.username).equals('user%40');
assume(parsed.password).equals('pas%3As%40');
assume(parsed.hostname).equals('www.example.com');
assume(parsed.pathname).equals('/');
assume(parsed.href).equals('http://user%40:pas%3As%40@www.example.com/');
});

it('adds @ to href if auth and host are empty', function () {
var parsed, i = 0;
var urls = [
'http:@/127.0.0.1',
'http::@/127.0.0.1',
'http:/@/127.0.0.1',
'http:/:@/127.0.0.1',
'http://@/127.0.0.1',
'http://:@/127.0.0.1',
'http:///@/127.0.0.1',
'http:///:@/127.0.0.1'
];

for (; i < urls.length; i++) {
parsed = parse(urls[i]);

assume(parsed.protocol).equals('http:');
assume(parsed.auth).equals('');
assume(parsed.username).equals('');
assume(parsed.password).equals('');
assume(parsed.host).equals('');
assume(parsed.hostname).equals('');
assume(parsed.pathname).equals('/127.0.0.1');
assume(parsed.origin).equals('null');
assume(parsed.href).equals('http://@/127.0.0.1');
assume(parsed.toString()).equals('http://@/127.0.0.1');
}

urls = [
'http:@/',
'http:@',
'http::@/',
'http::@',
'http:/@/',
'http:/@',
'http:/:@/',
'http:/:@',
'http://@/',
'http://@',
'http://:@/',
'http://:@'
];

for (i = 0; i < urls.length; i++) {
parsed = parse(urls[i]);

assume(parsed.protocol).equals('http:');
assume(parsed.auth).equals('');
assume(parsed.username).equals('');
assume(parsed.password).equals('');
assume(parsed.host).equals('');
assume(parsed.hostname).equals('');
assume(parsed.pathname).equals('/');
assume(parsed.origin).equals('null');
assume(parsed.href).equals('http:///');
assume(parsed.toString()).equals('http:///');
}
});
});

it('accepts multiple ???', function () {
@@ -1124,6 +1265,26 @@ describe('url-parse', function () {
assume(data.username).equals('');
assume(data.password).equals('quux');
assume(data.href).equals('https://:quux@example.com/');

assume(data.set('auth', 'user@:pass@')).equals(data);
assume(data.username).equals('user%40');
assume(data.password).equals('pass%40');
assume(data.href).equals('https://user%40:pass%40@example.com/');

assume(data.set('auth', 'user%40:pass%40')).equals(data);
assume(data.username).equals('user%40');
assume(data.password).equals('pass%40');
assume(data.href).equals('https://user%40:pass%40@example.com/');

assume(data.set('auth', 'user:pass:word')).equals(data);
assume(data.username).equals('user');
assume(data.password).equals('pass%3Aword');
assume(data.href).equals('https://user:pass%3Aword@example.com/');

assume(data.set('auth', 'user:pass%3Aword')).equals(data);
assume(data.username).equals('user');
assume(data.password).equals('pass%3Aword');
assume(data.href).equals('https://user:pass%3Aword@example.com/');
});

it('updates other values', function () {