fix(security): env-configurable trustProxy with a safe default (#61)
trustProxy was unconditionally true, so req.ip came from a client-forgeable X-Forwarded-For and the per-IP throttles (share-AI, /mcp brute-force) were spoofable. Make it env-configurable (TRUST_PROXY) with a safe default that trusts XFF only from loopback/private proxies, documented in .env.example. NOTE: this changes the default from trust-all; deployments whose proxy is on a public IP must set TRUST_PROXY (caveat documented). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
25
.env.example
25
.env.example
@@ -3,14 +3,31 @@ APP_URL=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
# --- Security / reverse proxy ---
|
||||
# The app runs with Fastify `trustProxy` ENABLED, so it derives the client IP
|
||||
# (req.ip) from the `X-Forwarded-For` header. That header is client-forgeable.
|
||||
# Deploy this app behind a trusted reverse proxy that SETS/OVERWRITES (not
|
||||
# appends) `X-Forwarded-For` with the real client IP. Without such a proxy, any
|
||||
# The app derives the client IP (req.ip) from the `X-Forwarded-For` header via
|
||||
# Fastify `trustProxy`. That header is client-forgeable, so XFF is trusted only
|
||||
# from proxies on the configured trusted networks. Deploy this app behind a
|
||||
# trusted reverse proxy that SETS/OVERWRITES (not appends) `X-Forwarded-For`
|
||||
# with the real client IP. If XFF is trusted from an untrusted source, any
|
||||
# per-IP throttling — including the /mcp Basic brute-force limiter — can be
|
||||
# bypassed by an attacker who simply spoofs `X-Forwarded-For` to rotate IPs.
|
||||
# (The /mcp limiter keeps a global per-email key as an IP-independent backstop,
|
||||
# but the per-IP and per-IP+email keys rely on a trustworthy X-Forwarded-For.)
|
||||
#
|
||||
# TRUST_PROXY controls which proxies are trusted to set X-Forwarded-For.
|
||||
# Default (unset/empty): `loopback, linklocal, uniquelocal` — XFF is trusted
|
||||
# ONLY from private/loopback proxies, so a public-IP client cannot spoof req.ip.
|
||||
# This is the safe default for the common case where the reverse proxy runs on
|
||||
# loopback or a private network; req.ip still resolves to the real client.
|
||||
# WARNING: this changed the previous default of trust-all. If your reverse proxy
|
||||
# sits on a PUBLIC IP, the default will NOT trust its XFF and req.ip will be the
|
||||
# proxy's IP — set TRUST_PROXY accordingly. Accepted values:
|
||||
# - true restore trust-all (ONLY safe if a trusted proxy ALWAYS overwrites
|
||||
# X-Forwarded-For; otherwise clients can spoof their IP)
|
||||
# - false never trust X-Forwarded-For (req.ip is the socket peer)
|
||||
# - <int> number of trusted proxy hops in front of the app
|
||||
# - <list> comma-separated CIDR/IP list of trusted proxies, e.g.
|
||||
# `127.0.0.1, 10.0.0.0/8`
|
||||
# TRUST_PROXY=
|
||||
|
||||
# minimum of 32 characters. Generate one with: openssl rand -hex 32
|
||||
APP_SECRET=REPLACE_WITH_LONG_SECRET
|
||||
|
||||
@@ -15,11 +15,24 @@ import { InternalLogFilter } from './common/logger/internal-log-filter';
|
||||
import { EnvironmentService } from './integrations/environment/environment.service';
|
||||
import { resolveFrameHeader } from './common/helpers';
|
||||
|
||||
// Trust X-Forwarded-For ONLY from real proxies on private/loopback nets by
|
||||
// default, so a public-IP client cannot spoof its IP via X-Forwarded-For.
|
||||
// TRUST_PROXY env overrides: 'true'/'false', a hop count (integer), or a
|
||||
// CIDR/IP list string passed through to Fastify/proxy-addr.
|
||||
function resolveTrustProxy(rawInput?: string): boolean | number | string {
|
||||
const raw = rawInput?.trim();
|
||||
if (raw == null || raw === '') return 'loopback, linklocal, uniquelocal';
|
||||
if (raw === 'true') return true;
|
||||
if (raw === 'false') return false;
|
||||
const n = Number(raw);
|
||||
return Number.isInteger(n) && n >= 0 ? n : raw;
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({
|
||||
trustProxy: true,
|
||||
trustProxy: resolveTrustProxy(process.env.TRUST_PROXY),
|
||||
routerOptions: {
|
||||
maxParamLength: 1000,
|
||||
ignoreTrailingSlash: true,
|
||||
|
||||
Reference in New Issue
Block a user