Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions __tests__/request/ip-port.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

const { describe, it } = require('node:test')
const assert = require('node:assert/strict')
const request = require('../../test-helpers/context').request

describe('req.ip with port in X-Forwarded-For', () => {
describe('when XFF contains IPv4 with port', () => {
it('should strip the port', () => {
const req = request()
req.app.proxy = true
req.header['x-forwarded-for'] = '1.2.3.4:8080, 5.6.7.8'
Comment on lines +8 to +12

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request() is not defined in this file (you imported the helper as Request). As written, the tests will throw a ReferenceError before assertions run. Rename the import to request or update the calls to use the imported helper consistently.

Copilot uses AI. Check for mistakes.
assert.strictEqual(req.ip, '1.2.3.4')
})
})

describe('when XFF contains IPv6 with port', () => {
it('should strip the port and brackets', () => {
const req = request()
req.app.proxy = true
req.header['x-forwarded-for'] = '[::1]:8080, 127.0.0.1'
assert.strictEqual(req.ip, '::1')
})
})

describe('when XFF contains plain IPv4 (no port)', () => {
it('should return the address as-is', () => {
const req = request()
req.app.proxy = true
req.header['x-forwarded-for'] = '1.2.3.4, 5.6.7.8'
assert.strictEqual(req.ip, '1.2.3.4')
})
})

describe('when XFF contains plain IPv6 (no port)', () => {
it('should return the address as-is', () => {
const req = request()
req.app.proxy = true
req.header['x-forwarded-for'] = '::1, 127.0.0.1'
assert.strictEqual(req.ip, '::1')
})
})
})
30 changes: 29 additions & 1 deletion lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const sp = require('./search-params.js')
const typeis = require('type-is')
const fresh = require('fresh')
const only = require('./only.js')
const forwarded = require('forwarded')
const util = require('util')

const IP = Symbol('context#ip')
Expand All @@ -23,6 +24,27 @@ const IP = Symbol('context#ip')
* Prototype.
*/

/**
* Strip port from an IP address string.
* Handles IPv4 (e.g. "1.2.3.4:8080") and bracketed IPv6 (e.g. "[::1]:8080").
* @param {string} addr
* @returns {string}
* @private
*/
function stripPort (addr) {
if (!addr) return ''
if (addr.charCodeAt(0) === 91 /* [ */) {
const end = addr.indexOf(']')
return end > 1 ? addr.slice(1, end) : addr
}
// IPv4 with port has exactly one colon; IPv6 has multiple
const lastColon = addr.lastIndexOf(':')
if (lastColon > 0 && addr.indexOf(':') === lastColon) {
return addr.slice(0, lastColon)
}
return addr
}

module.exports = {

/**
Expand Down Expand Up @@ -462,7 +484,13 @@ module.exports = {

get ip () {
if (!this[IP]) {
this[IP] = this.ips[0] || this.socket.remoteAddress || ''
// Use forwarded() to properly parse the X-Forwarded-For chain.
// forwarded() returns [socketAddr, ...xffEntriesReversed], so the
// first XFF entry (original client) is at the end of the array.
// See: https://github.com/koajs/koa/issues/827
const addrs = forwarded(this.req)
const addr = stripPort(this.ips[0]) || stripPort(addrs[addrs.length - 1]) || addrs[0] || ''
Comment on lines +487 to +492

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ip now calls forwarded(this.req) unconditionally. This means a spoofed X-Forwarded-For can influence request.ip even when app.proxy is false (since this.ips[0] will be empty and the code falls back to forwarded() output). It can also inadvertently fall back to X-Forwarded-For when a non-default app.proxyIpHeader is configured but missing on a request. Please preserve the previous trust model: only read forwarded headers when app.proxy is true (and ideally only via the configured proxyIpHeader), otherwise use socket.remoteAddress; then apply stripPort() to the selected value.

Suggested change
// Use forwarded() to properly parse the X-Forwarded-For chain.
// forwarded() returns [socketAddr, ...xffEntriesReversed], so the
// first XFF entry (original client) is at the end of the array.
// See: https://github.com/koajs/koa/issues/827
const addrs = forwarded(this.req)
const addr = stripPort(this.ips[0]) || stripPort(addrs[addrs.length - 1]) || addrs[0] || ''
let addr
// When behind a proxy, trust only the configured proxyIpHeader via this.ips.
if (this.app && this.app.proxy) {
const ips = this.ips
if (ips && ips.length > 0) {
addr = stripPort(ips[0])
}
}
// Fallback to the remote socket address when not behind a proxy
// or when no trusted forwarded header is available.
if (!addr) {
const socket = this.req && this.req.socket
const remoteAddress =
socket &&
(socket.remoteAddress ||
(socket.socket && socket.socket.remoteAddress))
addr = stripPort(remoteAddress || '')
}

Copilot uses AI. Check for mistakes.
this[IP] = addr
}
return this[IP]
},
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"destroy": "^1.2.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"forwarded": "^0.2.0",

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the forwarded dependency requires updating package-lock.json as well; otherwise npm ci/locked installs will fail in CI because the lockfile no longer matches package.json. Please regenerate and commit the updated lockfile.

Suggested change
"forwarded": "^0.2.0",

Copilot uses AI. Check for mistakes.
"fresh": "~0.5.2",
"http-assert": "^1.5.0",
"http-errors": "^2.0.0",
Expand Down
Loading