Koa's ctx.hostname API performs naive parsing of the HTTP Host header, extracting everything before the first colon without validating the input conforms to RFC 3986 hostname syntax. When a malformed Host header containing a @ symbol (e.g., evil.com:fake@legitimate.com) is received, ctx.hostname returns evil.com - an attacker-controlled value. Applications using ctx.hostname for URL generation, password reset links, email verification URLs, or routing decisions are vulnerable to Host header injection attacks.
The vulnerability exists in Koa's hostname getter in lib/request.js:
// Koa 2.16.1 - lib/request.js
get hostname() {
const host = this.host;
if (!host) return '';
if ('[' === host[0]) return this.URL.hostname || ''; // IPv6 literal
return host.split(':', 1)[0];
}
The host getter retrieves the raw header value with HTTP/2 and proxy support:
// Koa 2.16.1 - lib/request.js
get host() {
const proxy = this.app.proxy;
let host = proxy && this.get('X-Forwarded-Host');
if (!host) {
if (this.req.httpVersionMajor >= 2) host = this.get(':authority');
if (!host) host = this.get('Host');
}
if (!host) return '';
return host.split(',')[0].trim();
}
The parsing logic simply splits on the first : and returns the first segment. There is no validation that the resulting string is a valid hostname per RFC 3986 Section 3.2.2.
RFC 3986 Section 3.2.2 defines the host component as:
host = IP-literal / IPv4address / reg-name
reg-name = *( unreserved / pct-encoded / sub-delims )
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
The @ character is explicitly NOT permitted in the host component - it is the delimiter separating userinfo from host in the authority component.
When an attacker sends:
Host: evil.com:fake@legitimate.com:3000
Koa...
2.16.43.1.2Exploitability
AV:NAC:LPR:NUI:NScope
S:UImpact
C:NI:HA:N7.5/CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N