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:
@@ -78,7 +78,7 @@ try {
|
|||||||
if (!eintraege.length) { console.log("Protokoll ist leer."); break; }
|
if (!eintraege.length) { console.log("Protokoll ist leer."); break; }
|
||||||
console.log("");
|
console.log("");
|
||||||
for (const e of eintraege) {
|
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 || ""}`);
|
+ `${(e.rolle || "—").padEnd(8)} ${(e.detail || "").padEnd(18)} ${e.ip || ""}`);
|
||||||
}
|
}
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|||||||
+25
-3
@@ -122,6 +122,28 @@ function db() {
|
|||||||
|
|
||||||
const jetzt = () => new Date().toISOString();
|
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) {
|
function hashe(code, salt, N) {
|
||||||
return scryptSync(code, salt, SCRYPT.keylen,
|
return scryptSync(code, salt, SCRYPT.keylen,
|
||||||
{ N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex");
|
{ 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();
|
const bis = new Date(Date.now() + SITZUNG_STUNDEN * 3600_000).toISOString();
|
||||||
db().prepare(
|
db().prepare(
|
||||||
"INSERT INTO sitzungen (token_hash, person_id, erstellt, gueltig_bis, ip, browser) VALUES (?,?,?,?,?,?)"
|
"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));
|
(req.get("user-agent") || "").slice(0, 200));
|
||||||
|
|
||||||
res.cookie(COOKIE, token, {
|
res.cookie(COOKIE, token, {
|
||||||
@@ -212,7 +234,7 @@ workspaceRouter.use((req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||||
const ip = req.ip || "?";
|
const ip = echteIp(req);
|
||||||
try {
|
try {
|
||||||
if (zuVieleVersuche(ip)) {
|
if (zuVieleVersuche(ip)) {
|
||||||
protokolliere("anmeldung_gesperrt", { ip });
|
protokolliere("anmeldung_gesperrt", { ip });
|
||||||
@@ -266,7 +288,7 @@ workspaceRouter.post("/workspace/api/abmelden", (req, res) => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
const person = sitzungLesen(req);
|
const person = sitzungLesen(req);
|
||||||
db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token));
|
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. */ }
|
} catch { /* Abmelden darf nie scheitern. */ }
|
||||||
res.clearCookie(COOKIE, { path: "/workspace" });
|
res.clearCookie(COOKIE, { path: "/workspace" });
|
||||||
|
|||||||
Reference in New Issue
Block a user