Feature: Add configurable X-Real-IP header support for rate limiting (#3504)

* fix: correct client IP detection behind reverse proxy

* Revert "fix: correct client IP detection behind reverse proxy"

This reverts commit 7207506240.

* feat: support X-Real-IP header for reverse proxy setups

* feat: add option to use x-real-ip for rate limiting behind reverse proxy

* docs: update rate limiting configuration comments for X-Real-IP usage

* refactor: extract getIpAddress function to reduce code duplication

* revert(whitelist): rate limit settings shouldn't affect whitelist
This commit is contained in:
KevinSun
2025-02-21 03:11:44 +08:00
committed by GitHub
parent 3bb8b887e1
commit 3f5b63bba0
3 changed files with 29 additions and 7 deletions

View File

@ -85,6 +85,11 @@ cookieSecret: ''
disableCsrfProtection: false
# Disable startup security checks - NOT RECOMMENDED
securityOverride: false
# -- RATE LIMITING CONFIGURATION --
rateLimiting:
# Use X-Real-IP header instead of socket IP for rate limiting
# Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
preferRealIpHeader: false
# -- ADVANCED CONFIGURATION --
# Open the browser automatically
autorun: true

View File

@ -3,13 +3,16 @@ import crypto from 'node:crypto';
import storage from 'node-persist';
import express from 'express';
import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
import { jsonParser, getIpFromRequest } from '../express-common.js';
import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js';
import { color, Cache, getConfigValue } from '../util.js';
import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false);
const MFA_CACHE = new Cache(5 * 60 * 1000);
const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
export const router = express.Router();
const loginLimiter = new RateLimiterMemory({
points: 5,
@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
return response.status(400).json({ error: 'Missing required fields' });
}
const ip = getIpFromRequest(request);
const ip = getIpAddress(request);
await loginLimiter.consume(ip);
/** @type {import('../users.js').User} */
@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => {
return response.json({ handle: user.handle });
} catch (error) {
if (error instanceof RateLimiterRes) {
console.error('Login failed: Rate limited from', getIpFromRequest(request));
console.error('Login failed: Rate limited from', getIpAddress(request));
return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });
}
@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
return response.status(400).json({ error: 'Missing required fields' });
}
const ip = getIpFromRequest(request);
const ip = getIpAddress(request);
await recoverLimiter.consume(ip);
/** @type {import('../users.js').User} */
@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
return response.sendStatus(204);
} catch (error) {
if (error instanceof RateLimiterRes) {
console.error('Recover step 1 failed: Rate limited from', getIpFromRequest(request));
console.error('Recover step 1 failed: Rate limited from', getIpAddress(request));
return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
}
@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
/** @type {import('../users.js').User} */
const user = await storage.getItem(toKey(request.body.handle));
const ip = getIpFromRequest(request);
const ip = getIpAddress(request);
if (!user) {
console.error('Recover step 2 failed: User', request.body.handle, 'not found');
@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
return response.sendStatus(204);
} catch (error) {
if (error instanceof RateLimiterRes) {
console.error('Recover step 2 failed: Rate limited from', getIpFromRequest(request));
console.error('Recover step 2 failed: Rate limited from', getIpAddress(request));
return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
}

View File

@ -25,3 +25,17 @@ export function getIpFromRequest(req) {
}
return clientIp;
}
/**
* Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address.
* This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...).
* @param {import('express').Request} req Request object
* @returns {string} IP address of the client
*/
export function getRealIpFromHeader(req) {
if (req.headers['x-real-ip']) {
return req.headers['x-real-ip'].toString();
}
return getIpFromRequest(req);
}