Skip to content

Commit 422e539

Browse files
authored
feat: support app.proxyIPHeader and app.maxIpsCount to make ctx.ips more security
1 parent 4dc56f6 commit 422e539

File tree

5 files changed

+98
-6
lines changed

5 files changed

+98
-6
lines changed

docs/api/index.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ app.listen(3000);
114114
- `app.env` defaulting to the __NODE_ENV__ or "development"
115115
- `app.keys` array of signed cookie keys
116116
- `app.proxy` when true proxy header fields will be trusted
117-
- `app.subdomainOffset` offset of `.subdomains` to ignore [2]
117+
- `app.subdomainOffset` offset of `.subdomains` to ignore, default to 2
118+
- `app.proxyIpHeader` proxy ip header, default to `X-Forwarded-For`
119+
- `app.maxIpsCount` max ips read from proxy ip header, default to 0 (means infinity)
118120

119121
You can pass the settings to the constructor:
120122
```js

docs/api/request.md

+41-3
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ ctx.request.href;
9999

100100
Get hostname when present. Supports `X-Forwarded-Host`
101101
when `app.proxy` is __true__, otherwise `Host` is used.
102-
102+
103103
If host is IPv6, Koa delegates parsing to
104104
[WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api),
105105
*Note* This may impact performance.
@@ -193,8 +193,46 @@ ctx.body = await db.find('something');
193193
### request.ips
194194

195195
When `X-Forwarded-For` is present and `app.proxy` is enabled an array
196-
of these ips is returned, ordered from upstream -> downstream. When disabled
197-
an empty array is returned.
196+
of these ips is returned, ordered from upstream -> downstream. When
197+
disabled an empty array is returned.
198+
199+
For example if the value were "client, proxy1, proxy2",
200+
you would receive the array `["client", "proxy1", "proxy2"]`.
201+
202+
Most of the reverse proxy(nginx) set x-forwarded-for via
203+
`proxy_add_x_forwarded_for`, which poses a certain security risk.
204+
A malicious attacker can forge a client's ip address by forging
205+
a `X-Forwarded-For`request header. The request sent by the client
206+
has an `X-Forwarded-For` request header for 'forged'. After being
207+
forwarded by the reverse proxy, `request.ips` will be
208+
['forged', 'client', 'proxy1', 'proxy2'].
209+
210+
Koa offers two options to avoid being bypassed.
211+
212+
If you can control the reverse proxy, you can avoid bypassing
213+
by adjusting the configuration, or use the `app.proxyIpHeader`
214+
provided by koa to avoid reading `x-forwarded-for` to get ips.
215+
216+
```js
217+
const app = new Koa({
218+
proxy: true,
219+
proxyIpHeader: 'X-Real-IP',
220+
});
221+
```
222+
223+
If you know exactly how many reverse proxies are in front of
224+
the server, you can avoid reading the user's forged request
225+
header by configuring `app.maxIpsCount`:
226+
227+
```js
228+
const app = new Koa({
229+
proxy: true,
230+
maxIpsCount: 1, // only one proxy in front of the server
231+
});
232+
233+
// request.header['X-Forwarded-For'] === [ '127.0.0.1', '127.0.0.2' ];
234+
// ctx.ips === [ '127.0.0.2' ];
235+
```
198236

199237
### request.subdomains
200238

lib/application.js

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ module.exports = class Application extends Emitter {
4141
* @param {string[]} [options.keys] Signed cookie keys
4242
* @param {boolean} [options.proxy] Trust proxy headers
4343
* @param {number} [options.subdomainOffset] Subdomain offset
44+
* @param {boolean} [options.proxyIpHeader] proxy ip header, default to X-Forwarded-For
45+
* @param {boolean} [options.maxIpsCount] max ips read from proxy ip header, default to 0 (means infinity)
4446
*
4547
*/
4648

@@ -49,6 +51,8 @@ module.exports = class Application extends Emitter {
4951
options = options || {};
5052
this.proxy = options.proxy || false;
5153
this.subdomainOffset = options.subdomainOffset || 2;
54+
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
55+
this.maxIpsCount = options.maxIpsCount || 0;
5256
this.env = options.env || process.env.NODE_ENV || 'development';
5357
if (options.keys) this.keys = options.keys;
5458
this.middleware = [];

lib/request.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -432,10 +432,14 @@ module.exports = {
432432

433433
get ips() {
434434
const proxy = this.app.proxy;
435-
const val = this.get('X-Forwarded-For');
436-
return proxy && val
435+
const val = this.get(this.app.proxyIpHeader);
436+
let ips = proxy && val
437437
? val.split(/\s*,\s*/)
438438
: [];
439+
if (this.app.maxIpsCount > 0) {
440+
ips = ips.slice(-this.app.maxIpsCount);
441+
}
442+
return ips;
439443
},
440444

441445
/**

test/request/ips.js

+44
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,48 @@ describe('req.ips', () => {
2424
});
2525
});
2626
});
27+
28+
describe('when options.proxyIpHeader is present', () => {
29+
describe('and proxy is not trusted', () => {
30+
it('should be ignored', () => {
31+
const req = request();
32+
req.app.proxy = false;
33+
req.app.proxyIpHeader = 'x-client-ip';
34+
req.header['x-client-ip'] = '127.0.0.1,127.0.0.2';
35+
assert.deepEqual(req.ips, []);
36+
});
37+
});
38+
39+
describe('and proxy is trusted', () => {
40+
it('should be used', () => {
41+
const req = request();
42+
req.app.proxy = true;
43+
req.app.proxyIpHeader = 'x-client-ip';
44+
req.header['x-client-ip'] = '127.0.0.1,127.0.0.2';
45+
assert.deepEqual(req.ips, ['127.0.0.1', '127.0.0.2']);
46+
});
47+
});
48+
});
49+
50+
describe('when options.maxIpsCount is present', () => {
51+
describe('and proxy is not trusted', () => {
52+
it('should be ignored', () => {
53+
const req = request();
54+
req.app.proxy = false;
55+
req.app.maxIpsCount = 1;
56+
req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2';
57+
assert.deepEqual(req.ips, []);
58+
});
59+
});
60+
61+
describe('and proxy is trusted', () => {
62+
it('should be used', () => {
63+
const req = request();
64+
req.app.proxy = true;
65+
req.app.maxIpsCount = 1;
66+
req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2';
67+
assert.deepEqual(req.ips, ['127.0.0.2']);
68+
});
69+
});
70+
});
2771
});

0 commit comments

Comments
 (0)