aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-25 15:15:19 -0700
committerAdam Soltys <[email protected]>2026-09-25 15:15:19 -0700
commit56aef551bf72eb306d96b4b81b657c4a05037eb5 (patch)
tree993408a95fb4bcdad2e762cdc6c24cbd10bf6415 /lib
parentcd5c18867a3b6764cd2b6108906b40b4578a65b4 (diff)
downloadcoinos-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]>
Diffstat (limited to 'lib')
-rw-r--r--lib/app.ts30
1 files changed, 30 insertions, 0 deletions
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<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 = [