From 56aef551bf72eb306d96b4b81b657c4a05037eb5 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 25 Sep 2026 15:15:19 -0700 Subject: app: don't trust client IP headers on onion requests The tor container connects directly, bypassing Cloudflare, so the client could set cf-connecting-ip, x-forwarded-for and rate-limit-by to anything. Pin those requests to the tor container's address before the rate limiter runs. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/app.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/app.ts b/lib/app.ts index 65607d24..faf3d9bd 100644 --- a/lib/app.ts +++ b/lib/app.ts @@ -9,6 +9,7 @@ import fastifyStatic from "@fastify/static"; import fastify from "fastify"; import pino from "pino"; +import dns from "node:dns"; import * as path from "path"; import { jwtStrategy } from "$lib/auth"; @@ -72,6 +73,35 @@ const redactHeaders = (headers: any) => { // }); // +// The client IP comes from cf-connecting-ip, which Cloudflare sets and lb/ui +// pass through. Onion traffic skips Cloudflare: the tor container connects to +// us directly, so whatever cf-connecting-ip / x-forwarded-for / rate-limit-by +// the client sent would reach the rate limiter and IP bans untouched. Pin those +// requests to the tor container's own address instead. Registered before the +// rate limiter so its keyGenerator sees the rewritten header. +const UNTRUSTED_PEERS = ["tor"]; +let untrusted = new Set(); +const resolveUntrusted = async () => { + const next = new Set(); + for (const host of UNTRUSTED_PEERS) { + try { + for (const { address } of await dns.promises.lookup(host, { all: true })) + next.add(address); + } catch {} + } + if (next.size) untrusted = next; +}; +await resolveUntrusted(); +setInterval(resolveUntrusted, 30000).unref(); + +app.addHook("onRequest", async (req) => { + const peer = (req.socket.remoteAddress || "").replace(/^::ffff:/, ""); + if (!untrusted.has(peer)) return; + req.headers["cf-connecting-ip"] = peer; + delete req.headers["x-forwarded-for"]; + delete req.headers["rate-limit-by"]; +}); + app.addHook("preHandler", async (req) => { const url = req.raw.url; const ignore = [ -- cgit v1.2.3