🚦 fix: ERR_ERL_INVALID_IP_ADDRESS and IPv6 Key Collisions in IP Rate Limiters (#12319)

* fix: Add removePorts keyGenerator to all IP-based rate limiters

Six IP-based rate limiters are missing the `keyGenerator: removePorts`
option that is already used by the auth-related limiters (login,
register, resetPassword, verifyEmail). Without it, reverse proxies that
include ports in X-Forwarded-For headers cause
ERR_ERL_INVALID_IP_ADDRESS errors from express-rate-limit.

Fixes #12318

* fix: make removePorts IPv6-safe to prevent rate-limit key collisions

The original regex `/:\d+[^:]*$/` treated the last colon-delimited
segment of bare IPv6 addresses as a port, mangling valid IPs
(e.g. `::1` → `::`, `2001:db8::1` → `2001:db8::`). Distinct IPv6
clients could collapse into the same rate-limit bucket.

Use `net.isIP()` as a fast path for already-valid IPs, then match
bracketed IPv6+port and IPv4+port explicitly. Bare IPv6 addresses
are now returned unchanged.

Also fixes pre-existing property ordering inconsistency in
ttsLimiters.js userLimiterOptions (keyGenerator before store).

* refactor: move removePorts to packages/api as TypeScript, fix import order

- Move removePorts implementation to packages/api/src/utils/removePorts.ts
  with proper Express Request typing
- Reduce api/server/utils/removePorts.js to a thin re-export from
  @librechat/api for backward compatibility
- Consolidate removePorts import with limiterCache from @librechat/api
  in all 6 limiter files, fixing import order (package imports shortest
  to longest, local imports longest to shortest)
- Remove narrating inline comments per code style guidelines

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Brad Russell 2026-03-19 21:48:03 -04:00 committed by GitHub
parent 748fd086c1
commit ecd6d76bc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 162 additions and 28 deletions

View file

@ -1,8 +1,7 @@
const { logger } = require('@librechat/data-schemas');
const { isEnabled, math } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { isEnabled, math, removePorts } = require('@librechat/api');
const { deleteAllUserSessions } = require('~/models');
const { removePorts } = require('~/server/utils');
const getLogStores = require('./getLogStores');
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};

View file

@ -1,11 +1,10 @@
const { Keyv } = require('keyv');
const uap = require('ua-parser-js');
const { logger } = require('@librechat/data-schemas');
const { isEnabled, keyvMongo } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const denyRequest = require('./denyRequest');
const { isEnabled, keyvMongo, removePorts } = require('@librechat/api');
const { getLogStores } = require('~/cache');
const denyRequest = require('./denyRequest');
const { findUser } = require('~/models');
const banCache = new Keyv({ store: keyvMongo, namespace: ViolationTypes.BAN, ttl: 0 });

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache, removePorts } = require('@librechat/api');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
@ -59,6 +59,7 @@ const createForkLimiters = () => {
windowMs: forkIpWindowMs,
max: forkIpMax,
handler: createForkHandler(),
keyGenerator: removePorts,
store: limiterCache('fork_ip_limiter'),
};
const userLimiterOptions = {

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache, removePorts } = require('@librechat/api');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
@ -60,6 +60,7 @@ const createImportLimiters = () => {
windowMs: importIpWindowMs,
max: importIpMax,
handler: createImportHandler(),
keyGenerator: removePorts,
store: limiterCache('import_ip_limiter'),
};
const userLimiterOptions = {
@ -67,7 +68,7 @@ const createImportLimiters = () => {
max: importUserMax,
handler: createImportHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
return req.user?.id;
},
store: limiterCache('import_user_limiter'),
};

View file

@ -1,7 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache, removePorts } = require('@librechat/api');
const { logViolation } = require('~/cache');
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache, removePorts } = require('@librechat/api');
const denyRequest = require('~/server/middleware/denyRequest');
const { logViolation } = require('~/cache');
@ -50,6 +50,7 @@ const ipLimiterOptions = {
windowMs: ipWindowMs,
max: ipMax,
handler: createHandler(),
keyGenerator: removePorts,
store: limiterCache('message_ip_limiter'),
};
@ -58,7 +59,7 @@ const userLimiterOptions = {
max: userMax,
handler: createHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
return req.user?.id;
},
store: limiterCache('message_user_limiter'),
};

View file

@ -1,7 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache, removePorts } = require('@librechat/api');
const { logViolation } = require('~/cache');
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;

View file

@ -1,7 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache, removePorts } = require('@librechat/api');
const { logViolation } = require('~/cache');
const {

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache, removePorts } = require('@librechat/api');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
@ -54,6 +54,7 @@ const createSTTLimiters = () => {
windowMs: sttIpWindowMs,
max: sttIpMax,
handler: createSTTHandler(),
keyGenerator: removePorts,
store: limiterCache('stt_ip_limiter'),
};
@ -62,7 +63,7 @@ const createSTTLimiters = () => {
max: sttUserMax,
handler: createSTTHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
return req.user?.id;
},
store: limiterCache('stt_user_limiter'),
};

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache, removePorts } = require('@librechat/api');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
@ -54,6 +54,7 @@ const createTTSLimiters = () => {
windowMs: ttsIpWindowMs,
max: ttsIpMax,
handler: createTTSHandler(),
keyGenerator: removePorts,
store: limiterCache('tts_ip_limiter'),
};
@ -61,10 +62,10 @@ const createTTSLimiters = () => {
windowMs: ttsUserWindowMs,
max: ttsUserMax,
handler: createTTSHandler(false),
store: limiterCache('tts_user_limiter'),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
return req.user?.id;
},
store: limiterCache('tts_user_limiter'),
};
const ttsIpLimiter = rateLimit(ipLimiterOptions);

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache, removePorts } = require('@librechat/api');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {
@ -60,6 +60,7 @@ const createFileLimiters = () => {
windowMs: fileUploadIpWindowMs,
max: fileUploadIpMax,
handler: createFileUploadHandler(),
keyGenerator: removePorts,
store: limiterCache('file_upload_ip_limiter'),
};
@ -68,7 +69,7 @@ const createFileLimiters = () => {
max: fileUploadUserMax,
handler: createFileUploadHandler(false),
keyGenerator: function (req) {
return req.user?.id; // Use the user ID or NULL if not available
return req.user?.id;
},
store: limiterCache('file_upload_user_limiter'),
};

View file

@ -1,7 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache, removePorts } = require('@librechat/api');
const { logViolation } = require('~/cache');
const {

View file

@ -1,4 +1,3 @@
const removePorts = require('./removePorts');
const handleText = require('./handleText');
const sendEmail = require('./sendEmail');
const queue = require('./queue');
@ -6,7 +5,6 @@ const files = require('./files');
module.exports = {
...handleText,
removePorts,
sendEmail,
...files,
...queue,

View file

@ -1 +0,0 @@
module.exports = (req) => req?.ip?.replace(/:\d+[^:]*$/, '');