Workspace: echte Besucher-IP statt Cloudflare-Adresse

Gefunden beim Nachsehen im Protokoll nach der ersten echten Anmeldung:
Jeder Eintrag trug dieselbe IP 172.69.220.140 -- eine Cloudflare-Adresse.

Ursache: Die Kette ist Besucher -> Cloudflare -> Caddy -> Express, aber
`trust proxy` steht auf 1. Express nimmt daher den letzten Eintrag aus
X-Forwarded-For, und das ist Cloudflare.

Auswirkung war nicht nur ein unbrauchbares Protokoll, sondern vor allem:
ALLE Nutzer teilten sich einen einzigen Sperr-Zaehler. Beim ersten
Anmelden waren nach zwei Tippfehlern plus drei Testversuchen bereits
5 von 8 verbraucht -- drei weitere und der Zugang waere fuer alle
gesperrt gewesen.

Jetzt wird CF-Connecting-IP ausgewertet (setzt Cloudflare bei jeder
Anfrage selbst, vom Besucher nicht faelschbar), mit Rueckfall auf die
Peer-Adresse. `trust proxy` bleibt bewusst unangetastet, damit die
Aenderung nur /workspace betrifft und nicht die ganze Website.

Geprueft: 8 Fehlversuche von IP A sperren IP A (429), IP B bekommt
weiterhin 401 und kann sich normal anmelden (200).

Ausserdem: Spaltenbreite im Protokoll-Ausdruck korrigiert -- bei
`anmeldung_fehlgeschlagen` (genau 24 Zeichen) klebte die Rolle am Namen.
This commit is contained in:
2026-08-27 22:53:51 +02:00
parent f3a55af80f
commit 6d9bad880c
2 changed files with 26 additions and 4 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ try {
if (!eintraege.length) { console.log("Protokoll ist leer."); break; }
console.log("");
for (const e of eintraege) {
console.log(` ${e.zeitpunkt.slice(0, 19).replace("T", " ")} ${(e.aktion || "").padEnd(24)} `
console.log(` ${e.zeitpunkt.slice(0, 19).replace("T", " ")} ${(e.aktion || "").padEnd(26)}`
+ `${(e.rolle || "—").padEnd(8)} ${(e.detail || "").padEnd(18)} ${e.ip || ""}`);
}
console.log("");
+25 -3
View File
@@ -122,6 +122,28 @@ function db() {
const jetzt = () => new Date().toISOString();
/* Die echte Besucher-IP.
Warum das nötig ist: Die Kette lautet Besucher -> Cloudflare -> Caddy ->
Express. `trust proxy` steht auf 1, Express nimmt daher den letzten
Eintrag aus X-Forwarded-For -- und das ist die Adresse von Cloudflare,
nicht die des Besuchers. Folge: Im Protokoll stand bei jedem dieselbe
IP, und viel schlimmer -- ALLE Nutzer teilten sich einen einzigen
Sperr-Zähler. Ein paar Tippfehler von drei Leuten hätten den Zugang für
alle gesperrt.
Cloudflare setzt die echte Adresse in CF-Connecting-IP und überschreibt
den Kopf bei jeder Anfrage, ein Besucher kann ihn also nicht fälschen.
Einschränkung: Wer die Adresse des Servers kennt und ihn unter Umgehung
von Cloudflare direkt anspricht, könnte den Kopf frei setzen. Deshalb
wird auf die Peer-Adresse zurückgefallen, sobald der Kopf fehlt -- und
beide Werte landen im Protokoll, damit so etwas auffällt. */
function echteIp(req) {
const cf = req.get("cf-connecting-ip");
if (cf && cf.length <= 45) return cf.trim();
return req.ip || "?";
}
function hashe(code, salt, N) {
return scryptSync(code, salt, SCRYPT.keylen,
{ N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex");
@@ -180,7 +202,7 @@ function sitzungSetzen(res, person, req) {
const bis = new Date(Date.now() + SITZUNG_STUNDEN * 3600_000).toISOString();
db().prepare(
"INSERT INTO sitzungen (token_hash, person_id, erstellt, gueltig_bis, ip, browser) VALUES (?,?,?,?,?,?)"
).run(tokenHash(token), person.id, jetzt(), bis, req.ip || null,
).run(tokenHash(token), person.id, jetzt(), bis, echteIp(req),
(req.get("user-agent") || "").slice(0, 200));
res.cookie(COOKIE, token, {
@@ -212,7 +234,7 @@ workspaceRouter.use((req, res, next) => {
});
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
const ip = req.ip || "?";
const ip = echteIp(req);
try {
if (zuVieleVersuche(ip)) {
protokolliere("anmeldung_gesperrt", { ip });
@@ -266,7 +288,7 @@ workspaceRouter.post("/workspace/api/abmelden", (req, res) => {
if (token) {
const person = sitzungLesen(req);
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
if (person) protokolliere("abmeldung", { personId: person.id, rolle: person.rolle, ip: req.ip });
if (person) protokolliere("abmeldung", { personId: person.id, rolle: person.rolle, ip: echteIp(req) });
}
} catch { /* Abmelden darf nie scheitern. */ }
res.clearCookie(COOKIE, { path: "/workspace" });