diff options
| author | Adam Soltys <[email protected]> | 2026-09-25 15:15:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-25 15:15:19 -0700 |
| commit | 56aef551bf72eb306d96b4b81b657c4a05037eb5 (patch) | |
| tree | 993408a95fb4bcdad2e762cdc6c24cbd10bf6415 | |
| parent | cd5c18867a3b6764cd2b6108906b40b4578a65b4 (diff) | |
| download | coinos-server-56aef551bf72eb306d96b4b81b657c4a05037eb5.tar.gz coinos-server-56aef551bf72eb306d96b4b81b657c4a05037eb5.zip | |
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) <[email protected]>
| -rw-r--r-- | lib/app.ts | 30 |
1 files changed, 30 insertions, 0 deletions
@@ -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<string>(); +const resolveUntrusted = async () => { + const next = new Set<string>(); + 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 = [ |
