VYPR
High severityNVD Advisory· Published Feb 26, 2026· Updated Feb 26, 2026

Koa has Host Header Injection via `ctx.hostname`

CVE-2026-27959

Description

Koa is middleware for Node.js using ES2017 async functions. Prior to versions 3.1.2 and 2.16.4, 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 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. Versions 3.1.2 and 2.16.4 fix the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
koanpm
>= 3.0.0, < 3.1.23.1.2
koanpm
< 2.16.42.16.4

Affected products

1

Patches

2
b76ddc01fdb7

Merge commit from fork

https://github.com/koajs/koakillaFeb 25, 2026via ghsa
3 files changed · +83 1
  • lib/request.js+11 1 modified
    @@ -257,7 +257,17 @@ module.exports = {
           if (!host) host = this.get('Host');
         }
         if (!host) return '';
    -    return splitCommaSeparatedValues(host, 1)[0];
    +    host = splitCommaSeparatedValues(host, 1)[0];
    +    // Host header may contain userinfo (e.g., "user@host") which is invalid per RFC 7230.
    +    // Use URL parser to correctly extract the host portion.
    +    if (host.includes('@')) {
    +      try {
    +        host = new URL(`http://${host}`).host;
    +      } catch (e) {
    +        return '';
    +      }
    +    }
    +    return host;
       },
     
       /**
    
  • __tests__/request/host.js+44 0 modified
    @@ -94,4 +94,48 @@ describe('req.host', () => {
           });
         });
       });
    +
    +  describe('with Host header containing @', () => {
    +    it('should correctly parse host from userinfo@host format', () => {
    +      const req = request();
    +      req.header.host = 'evil.com:fake@legitimate.com';
    +      assert.strictEqual(req.host, 'legitimate.com');
    +    });
    +
    +    it('should correctly parse host from user@host format', () => {
    +      const req = request();
    +      req.header.host = 'user@example.com';
    +      assert.strictEqual(req.host, 'example.com');
    +    });
    +
    +    it('should correctly parse host with port from userinfo@host:port format', () => {
    +      const req = request();
    +      req.header.host = 'user:pass@example.com:8080';
    +      assert.strictEqual(req.host, 'example.com:8080');
    +    });
    +
    +    it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
    +      const req = request();
    +      req.app.proxy = true;
    +      req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com';
    +      req.header.host = 'foo.com';
    +      assert.strictEqual(req.host, 'legitimate.com');
    +    });
    +
    +    it('should correctly parse @ in :authority on HTTP/2', () => {
    +      const req = request({
    +        httpVersionMajor: 2,
    +        httpVersion: '2.0'
    +      });
    +      req.header[':authority'] = 'evil.com:fake@legitimate.com';
    +      req.header.host = 'foo.com';
    +      assert.strictEqual(req.host, 'legitimate.com');
    +    });
    +
    +    it('should return empty string for invalid host with @', () => {
    +      const req = request();
    +      req.header.host = 'user@';
    +      assert.strictEqual(req.host, '');
    +    });
    +  });
     });
    
  • __tests__/request/hostname.js+28 0 modified
    @@ -70,4 +70,32 @@ describe('req.hostname', () => {
           });
         });
       });
    +
    +  describe('with Host header containing @', () => {
    +    it('should correctly parse hostname from userinfo@host format', () => {
    +      const req = request();
    +      req.header.host = 'evil.com:fake@legitimate.com';
    +      assert.strictEqual(req.hostname, 'legitimate.com');
    +    });
    +
    +    it('should correctly parse hostname from user@host format', () => {
    +      const req = request();
    +      req.header.host = 'user@example.com';
    +      assert.strictEqual(req.hostname, 'example.com');
    +    });
    +
    +    it('should correctly parse hostname with port from userinfo@host:port format', () => {
    +      const req = request();
    +      req.header.host = 'user:pass@example.com:8080';
    +      assert.strictEqual(req.hostname, 'example.com');
    +    });
    +
    +    it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
    +      const req = request();
    +      req.app.proxy = true;
    +      req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com';
    +      req.header.host = 'foo.com';
    +      assert.strictEqual(req.hostname, 'legitimate.com');
    +    });
    +  });
     });
    
55ab9bab044e

Merge commit from fork

https://github.com/koajs/koakillaFeb 25, 2026via ghsa
3 files changed · +83 1
  • lib/request.js+11 1 modified
    @@ -256,7 +256,17 @@ module.exports = {
           if (!host) host = this.get('Host')
         }
         if (!host) return ''
    -    return splitCommaSeparatedValues(host, 1)[0]
    +    host = splitCommaSeparatedValues(host, 1)[0]
    +    // Host header may contain userinfo (e.g., "user@host") which is invalid per RFC 7230.
    +    // Use URL parser to correctly extract the host portion.
    +    if (host.includes('@')) {
    +      try {
    +        host = new URL(`http://${host}`).host
    +      } catch (e) {
    +        return ''
    +      }
    +    }
    +    return host
       },
     
       /**
    
  • __tests__/request/hostname.test.js+28 0 modified
    @@ -70,4 +70,32 @@ describe('req.hostname', () => {
           })
         })
       })
    +
    +  describe('with Host header containing @', () => {
    +    it('should correctly parse hostname from userinfo@host format', () => {
    +      const req = request()
    +      req.header.host = 'evil.com:fake@legitimate.com'
    +      assert.strictEqual(req.hostname, 'legitimate.com')
    +    })
    +
    +    it('should correctly parse hostname from user@host format', () => {
    +      const req = request()
    +      req.header.host = 'user@example.com'
    +      assert.strictEqual(req.hostname, 'example.com')
    +    })
    +
    +    it('should correctly parse hostname with port from userinfo@host:port format', () => {
    +      const req = request()
    +      req.header.host = 'user:pass@example.com:8080'
    +      assert.strictEqual(req.hostname, 'example.com')
    +    })
    +
    +    it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
    +      const req = request()
    +      req.app.proxy = true
    +      req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com'
    +      req.header.host = 'foo.com'
    +      assert.strictEqual(req.hostname, 'legitimate.com')
    +    })
    +  })
     })
    
  • __tests__/request/host.test.js+44 0 modified
    @@ -94,4 +94,48 @@ describe('req.host', () => {
           })
         })
       })
    +
    +  describe('with Host header containing @', () => {
    +    it('should correctly parse host from userinfo@host format', () => {
    +      const req = request()
    +      req.header.host = 'evil.com:fake@legitimate.com'
    +      assert.strictEqual(req.host, 'legitimate.com')
    +    })
    +
    +    it('should correctly parse host from user@host format', () => {
    +      const req = request()
    +      req.header.host = 'user@example.com'
    +      assert.strictEqual(req.host, 'example.com')
    +    })
    +
    +    it('should correctly parse host with port from userinfo@host:port format', () => {
    +      const req = request()
    +      req.header.host = 'user:pass@example.com:8080'
    +      assert.strictEqual(req.host, 'example.com:8080')
    +    })
    +
    +    it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
    +      const req = request()
    +      req.app.proxy = true
    +      req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com'
    +      req.header.host = 'foo.com'
    +      assert.strictEqual(req.host, 'legitimate.com')
    +    })
    +
    +    it('should correctly parse @ in :authority on HTTP/2', () => {
    +      const req = request({
    +        httpVersionMajor: 2,
    +        httpVersion: '2.0'
    +      })
    +      req.header[':authority'] = 'evil.com:fake@legitimate.com'
    +      req.header.host = 'foo.com'
    +      assert.strictEqual(req.host, 'legitimate.com')
    +    })
    +
    +    it('should return empty string for invalid host with @', () => {
    +      const req = request()
    +      req.header.host = 'user@'
    +      assert.strictEqual(req.host, '')
    +    })
    +  })
     })
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

5

News mentions

0

No linked articles in our index yet.