Repository: express-rate-limit/express-rate-limit

Issue:
Support for Forwarded http header (RFC7239) that supersedes X-Forwarded-XXX headers
### Description

RFC7239 Forwarded HTTP Extension supersedes the defacto standard of X-Forwarded-XXX (For, Host, Proto, Server, etc) with a true specification. That details information like the For parameter may include both IP addresses and Port numbers. 

A single header has the ability to contain the following parameters. 

Parameters

   This document specifies a number of parameters and valid values for
   each of them:

   o  "by" identifies the user-agent facing interface of the proxy.

   o  "for" identifies the node making the request to the proxy.

   o  "host" is the host request header field as received by the proxy.

   o  "proto" indicates what protocol was used to make the request.

A request chain with multiple proxies will inject their own information using a comma delimited

Forwarded: For=192.168.0.1;by=x.x.x.x;host=xxxx.yoyodyne,com;proto=https, for=192.168.0.1; ....

This header attempts to unravel the confusion that may be created by existing syntaxes used by different proxies.

### Why

Even though this RFC is 10 years old, it is seeing an increase in the adoption with commercial and open source proxy applications. To keep express-rate-limit viable with these proxies it will need to support this new HTTP header. 

### Alternatives

none

Hints from issue comments:
@nfriedly: Hey, sorry I missed this, I think I was out when it was first opened.

Currently express-rate-limit doesn't parse x-forwarded-for headers, it just accepts whatever express reports as the IP address. Express uses https://www.npmjs.com/package/proxy-addr to determine the IP, which in turn uses https://www.npmjs.com/package/forwarded to parse those headers. So, ideally, that would be the place to add support for the new Forwarded header. (Edit: both proxy-addr and forwarded have issues requesting support: https://github.com/jshttp/proxy-addr/issues/20 & https://github.com/jshttp/forwarded/issues/7 - the later is closed but the former is still open.)

However, it looks like there's a library, https://www.npmjs.com/package/forwarded-parse that handles the RFC 7239 header. So maybe it would make more sense to request that proxy-addr support both?

In the meanwhile, you could do something like this:

```js
var parseForwardedHeader = require('forwarded-parse');

app.use(rateLimit({
  // ...
  keyGenerator: (req, res) {
    if (req.headers.forwarded) {
      var results = parseForwardedHeader(req.headers.forwarded);
      if (results.length > 0) {
        var result = results.pop(); // or perhaps .shift() - I'm not sure if the order if there's more than one result
        return result.for;
      }
    return req.ip; // fallback
  }
}));
```

I didn't test the above, but hopefully it's enough to get you on the right track.

I'll leave this open until we have a better solution.@nfriedly: I recently read https://adam-p.ca/blog/2022/03/x-forwarded-for/ and it reinforced my opinion that the library shouldn't be processing the header _automatically_, but it also made me realize that we could at least warn users that the `Forwarded` header is present but being ignored, similar to what we already do for `X-Forwarded-For` if it's present but express isn't configured to use it. We could also look for various others like `X-Real-IP` (Nginx), `CF-Connecting-IP` (Cloudflare), or `True-Client-IP` (Cloudflare and Akamai), and the docs could clarify settings & conditions they need to have to trust those headers, and then offer an example like the one above.

I'm still a little iffy about adding support directly to this library, but we'll see where the discussion on https://github.com/jshttp/proxy-addr/issues/20 goes. It sounds like express and forwarded don't want to add support, so if proxy-addr also decides to go that way, I might add support here or find/make a library that handles it the way I want.@nfriedly: OK, I have a [PR up](https://github.com/express-rate-limit/express-rate-limit/pull/549) that will at least warn users when the `Forwarded` header is set but the default `keyGenerator` (which ignores it) is used. It also has sample code similar to above, but updated slightly to handle IPv6 shenanigans.

I might go back and add some of the other headers, but they're different enough that I might save it for a separate PR.

Solution patch:
diff --git a/docs/guides/troubleshooting-proxy-issues.mdx b/docs/guides/troubleshooting-proxy-issues.mdx
index 3d59cc9..103ff0c 100644
--- a/docs/guides/troubleshooting-proxy-issues.mdx
+++ b/docs/guides/troubleshooting-proxy-issues.mdx
@@ -12,7 +12,8 @@ request might be the IP of the load balancer/reverse proxy (making the rate
 limiter effectively a global one and blocking all requests once the limit is
 reached) or `undefined`.
 
-To solve this issue, please follow the steps given below.
+To solve this issue, assuming the proxy (or proxies) set the `X-Forwarded-For`
+header, please follow the steps given below.
 
 <Steps>
 
@@ -47,6 +48,14 @@ To solve this issue, please follow the steps given below.
 For more information about the `trust proxy` setting, take a look at the
 [official Express documentation](https://expressjs.com/en/guide/behind-proxies.html).
 
+#### Forwarded header
+
+If the server instead sets the `Forwarded` header, the steps are similar except
+that you'll need a
+[custom keyGenerator](/reference/error-codes#err-erl-forwarded-header) because
+express does not have built-in support for the `Forwarded` header (as of version
+5.1.0),
+
 ### Port Numbers in IP Addresses
 
 Sometimes, a problem arises because the format of the `X-Forwarded-For` header
diff --git a/docs/reference/changelog.mdx b/docs/reference/changelog.mdx
index cc97fe3..291d60d 100644
--- a/docs/reference/changelog.mdx
+++ b/docs/reference/changelog.mdx
@@ -9,6 +9,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to
 [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [8.1.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v8.1.0)
+
+### Fixed
+
+- `RateLimit-Reset` is now always set when `standardHeaders` is set to
+  `'draft-6'` and the store supports it.
+
+### Added
+
+- New `windowMs` validation check that ensures it's in the valid range when
+  using the built-in Memory store.
+- New `forwardedHeader` validation check to warn when the `Forwarded` header is
+  present but ignored.
+
 ## [8.0.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v8.0.1)
 
 ### Fixed
diff --git a/docs/reference/configuration.mdx b/docs/reference/configuration.mdx
index 1d72a23..9690261 100644
--- a/docs/reference/configuration.mdx
+++ b/docs/reference/configuration.mdx
@@ -508,6 +508,7 @@ Supported validations are:
 - [ip](/reference/error-codes#err-erl-undefined-ip-address)
 - [trustProxy](/reference/error-codes#err-erl-permissive-trust-proxy)
 - [xForwardedForHeader](/reference/error-codes#err-erl-unexpected-x-forwarded-for)
+- [forwardedHeader](/reference/error-codes#err-erl-forwarded-header)
 - [positiveHits](/reference/error-codes#err-erl-invalid-hits)
 - [unsharedStore](/reference/error-codes#err-erl-store-reuse)
 - [singleCount](/reference/error-codes#err-erl-double-count)
diff --git a/docs/reference/error-codes.mdx b/docs/reference/error-codes.mdx
index 9b84192..e8d9e77 100644
--- a/docs/reference/error-codes.mdx
+++ b/docs/reference/error-codes.mdx
@@ -96,6 +96,70 @@ This check will be prevented if a custom `keyGenerator` is supplied.
 Set `validate: {xForwardedForHeader: false}` in the options to disable the
 check.
 
+### `ERR_ERL_FORWARDED_HEADER`
+
+> Added in `8.1.0`.
+
+This error is logged when the `Forwarded` header
+([RFC 7239](https://tools.ietf.org/html/rfc7239)) is set (indicating use of a
+proxy) and the default [`keyGenerator`] is used.
+
+As of express@5.1.0, express's `trust proxy` setting does not support the
+`Forwarded` header, only `X-Forwarded-For`, and express-rate-limit relies on
+express to determine the requester's IP address, so this header is ignored in
+the default configuration.
+
+(There is an
+[open ticket requesting support be added to the proxy-addr library](https://github.com/jshttp/proxy-addr/issues/20)
+that express uses to determine IP address.)
+
+To use this header, add a custom [keyGenerator] that parses it and returns the
+correct IP. For example, using the
+[forwarded-parse](https://www.npmjs.com/package/forwarded-parse) library:
+
+```js
+import parseForwarded from 'forwarded-parse'
+import { rateLimit, ipKeyGenerator } from 'express-rate-limit'
+
+// ...
+
+const NUMBER_OF_PROXIES_TO_TRUST = 1
+
+app.use(
+	rateLimit({
+		keyGenerator: (req, res) => {
+			let ip = req.ip
+			try {
+				const forwards = parseForwarded(req.headers.forwarded)
+				ip = forwards[forwards.length - NUMBER_OF_PROXIES_TO_TRUST].for
+			} catch (ex) {
+				console.error(
+					`Error parsing Forwarded header ${req.headers.forwarded} from ${req.ip}:`,
+					ex,
+				)
+			}
+			return ipKeyGenerator(ip)
+		},
+		// ...
+	}),
+)
+```
+
+(`ipKeyGenerator` is needed for proper limiting of IPv6 users, see
+[ERR_ERL_KEY_GEN_IPV6](#err-erl-ipv6subnet-or-keygenerator) for more info.)
+
+If there are multiple proxies between your server and the internet and each adds
+a `Forwarded` header, increment `NUMBER_OF_PROXIES_TO_TRUST` to match.
+
+See [Troubleshooting Proxy Issues](/guides/troubleshooting-proxy-issues) for
+additional information.
+
+`req.ip` could also be set before express-rate-limit processes the request. If
+it is set to anything other than the default value (`req.socket.remoteAddress`),
+this error will be automatically suppressed.
+
+Set `validate: {forwardedHeader: false}` in the options to disable the check.
+
 ### `ERR_ERL_INVALID_HITS`
 
 > Added in `6.10.0`.
@@ -383,12 +447,13 @@ check.
 
 Node.js (as of version 24.6.0)
 [limits `setInterval()` delays to the max 32-bit signed integer value of `2147483647` milliseconds](https://nodejs.org/api/timers.html#setintervalcallback-delay-args),
-which is about 28.4 days. Accordingly, express-rate-limit checks the value
-before using it in the default MemoryStore and warns when it's out of range.
+which is about 28.4 days. Accordingly, express-rate-limit checks the `windowMs`
+value before using it in the default MemoryStore and warns when it's out of
+range.
 
 Longer values can be used with most external [stores](/reference/stores).
 
-Set `validate: {limit: false}` in the options to disable the check.
+Set `validate: {windowMs: false}` in the options to disable the check.
 
 ## Warnings
 
@@ -443,3 +508,6 @@ option to replicate its behaviour, or use the recommended
 `standardHeaders: 'draft-7'` option instead.
 
 Set `validate: {draftPolliHeaders: false}` in the options to disable the check.
+
+[`keyGenerator`]: /reference/configuration#keygenerator
+[keyGenerator]: /reference/configuration#keygenerator
diff --git a/source/rate-limit.ts b/source/rate-limit.ts
index c83a04a..ac2c73b 100644
--- a/source/rate-limit.ts
+++ b/source/rate-limit.ts
@@ -227,6 +227,7 @@ const parseOptions = (passedOptions: Partial<Options>): Configuration => {
 			validations.ip(request.ip)
 			validations.trustProxy(request)
 			validations.xForwardedForHeader(request)
+			validations.forwardedHeader(request)
 
 			// Note: eslint thinks the ! is unnecessary but dts-bundle-generator disagrees
 			// biome-ignore lint/style/noNonNullAssertion: validations.ip is called above
diff --git a/source/validations.ts b/source/validations.ts
index 90bb485..1dc06ff 100644
--- a/source/validations.ts
+++ b/source/validations.ts
@@ -138,6 +138,26 @@ const validations = {
 		}
 	},
 
+	/**
+	 * Alert the user if the Forwarded header is set (standardized version of X-Forwarded-For - not supported by express as of version 5.1.0)
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	forwardedHeader(request: Request) {
+		if (
+			request.headers.forwarded &&
+			request.ip === request.socket?.remoteAddress
+		) {
+			// if req.ip is set to something else, assume it's correct and don't warn here
+			throw new ValidationError(
+				'ERR_ERL_FORWARDED_HEADER',
+				`The 'Forwarded' header (standardized X-Forwarded-For) is set but currently being ignored. Add a custom keyGenerator to use a value from this header.`,
+			)
+		}
+	},
+
 	/**
 	 * Ensures totalHits value from store is a positive integer.
 	 *



Test patch:
diff --git a/test/library/validation-test.ts b/test/library/validation-test.ts
index 4d0a3b5..cf9d71f 100644
--- a/test/library/validation-test.ts
+++ b/test/library/validation-test.ts
@@ -98,6 +98,42 @@ describe('validations tests', () => {
 		})
 	})
 
+	describe('forwardedHeader', () => {
+		it('should log an error when the Forwarded set to any value, but not when it is unset', () => {
+			validations.forwardedHeader({
+				headers: {},
+				ip: '1.2.3.4',
+				socket: { remoteAddress: '1.2.3.4' },
+			} as any)
+			expect(console.error).not.toHaveBeenCalled()
+
+			validations.forwardedHeader({
+				headers: { forwarded: '5.6.7.8' },
+				ip: '1.2.3.4',
+				socket: { remoteAddress: '1.2.3.4' },
+			} as any)
+			expect(console.error).toHaveBeenCalledWith(
+				expect.objectContaining({ code: 'ERR_ERL_FORWARDED_HEADER' }),
+			)
+		})
+
+		it('should not log an error when request.ip has been set to a non-default value', () => {
+			validations.forwardedHeader({
+				headers: {},
+				ip: '1.2.3.100',
+				socket: { remoteAddress: '1.2.3.4' },
+			} as any)
+			expect(console.error).not.toHaveBeenCalled()
+
+			validations.forwardedHeader({
+				headers: { forwarded: '5.6.7.8' },
+				ip: '1.2.3.100',
+				socket: { remoteAddress: '1.2.3.4' },
+			} as any)
+			expect(console.error).not.toHaveBeenCalled()
+		})
+	})
+
 	describe('positiveHits', () => {
 		it('should log an error if hits is non-numeric', () => {
 			validations.positiveHits(true)