Sicherheitskorrekturen am Gate + Mobil-Scroll-Bugfix (Audit-Funde 07.08.2026)
This commit is contained in:
@@ -25,9 +25,23 @@
|
||||
===================================================================== */
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
/* align-items:center zuerst als Fallback fuer sehr alte Browser, die "safe" nicht
|
||||
kennen (eine ungueltige Deklaration wird ignoriert, die vorige bleibt bestehen) --
|
||||
align-items:safe center sorgt dafuer, dass zentrierter Inhalt beim Scrollen nie am
|
||||
Anfang "abgeschnitten" wirkt. */
|
||||
display: flex; align-items: center; align-items: safe center; justify-content: center;
|
||||
min-height: 100vh; padding: 2rem 1.2rem; position: relative;
|
||||
overflow: hidden; background: radial-gradient(ellipse at 50% 30%, #12161d 0%, #06070a 62%, #030304 100%);
|
||||
/* BUGFIX (07.08.2026, Nutzer-Report vom Handy: "Code eintippen geht, aber dann ist
|
||||
der 'Weiter'-Knopf nirgendwo"): overflow:hidden auf dem BODY verhinderte jegliches
|
||||
Scrollen -- sobald die virtuelle Tastatur einen grossen Teil des Bildschirms
|
||||
einnimmt, ragte der Knopf unterhalb der sichtbaren Flaeche heraus und war komplett
|
||||
unerreichbar, ohne jede Moeglichkeit hinzuscrollen (mit echter Tastatur-Simulation
|
||||
nachgestellt und bestaetigt). overflow-y:auto erlaubt Scrollen NUR wenn wirklich
|
||||
noetig -- im Normalfall ohne Tastatur aendert sich optisch nichts. overflow-x
|
||||
bleibt hidden, das war der eigentliche urspruengliche Zweck (verhindert einen
|
||||
ungewollten horizontalen Scrollbalken durch die Aurora-Nebel-Kreise, die
|
||||
absichtlich ueber den Bildschirmrand hinausragen). */
|
||||
overflow-x: hidden; overflow-y: auto; background: radial-gradient(ellipse at 50% 30%, #12161d 0%, #06070a 62%, #030304 100%);
|
||||
}
|
||||
|
||||
/* ---------- Aurora-Nebel, langsam driftend (3 Farbtöne der Marke) ---------- */
|
||||
|
||||
+81
-9
@@ -53,19 +53,91 @@ export async function verifySession(token, secret) {
|
||||
}
|
||||
}
|
||||
|
||||
/* SICHERHEITSKORREKTUR (07.08.2026, Audit-Fund): "next" kam bisher direkt vom Besucher (Query-
|
||||
Parameter in gate.html -> POST-Body hier -> unveraendert in der Antwort zurueck -> Browser
|
||||
macht `location.href = data.next`). Die Pruefung "faengt mit / an und ist nicht genau /" laesst
|
||||
AUCH "//boese-seite.de" durch -- das ist eine protokoll-relative URL, der Browser haengt das
|
||||
aktuelle https:// davor und navigiert tatsaechlich auf die fremde Seite. Ein praeparierter Link
|
||||
wie "https://dogfather-universe.com/gate.html?next=%2F%2Fboese-seite.de" saehe aus wie die
|
||||
echte Seite (echte Domain!), wuerde nach der Code-Eingabe aber still auf eine Phishing-Seite
|
||||
umleiten. Fix: zusaetzlich "//" und "/\" (das interpretieren manche Browser genauso) explizit
|
||||
ausschliessen -- "next" darf NUR noch ein echter, eigener Pfad sein. */
|
||||
function isSafeNextPath(value) {
|
||||
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !value.startsWith("/\\") && value !== "/";
|
||||
}
|
||||
|
||||
/* Zeitsicherer Vergleich (07.08.2026, Audit-Fund) -- genau dasselbe Prinzip wie schon in
|
||||
server-internal/lib/auth.js: normales "===" bricht bei der ersten abweichenden Stelle ab, was
|
||||
theoretisch ueber viele Anfragen einen winzigen Zeitunterschied messbar macht. Praktisch schwer
|
||||
auszunutzen, aber kostenlos zu vermeiden -- die Schleife laeuft immer komplett durch. */
|
||||
function timingSafeEqual(a, b) {
|
||||
a = String(a == null ? "" : a);
|
||||
b = String(b == null ? "" : b);
|
||||
const len = Math.max(a.length, b.length);
|
||||
let mismatch = a.length === b.length ? 0 : 1;
|
||||
for (let i = 0; i < len; i++) {
|
||||
mismatch |= (i < a.length ? a.charCodeAt(i) : 0) ^ (i < b.length ? b.charCodeAt(i) : 0);
|
||||
}
|
||||
return mismatch === 0;
|
||||
}
|
||||
|
||||
/* SICHERHEITSKORREKTUR (07.08.2026, Audit-Fund): bisher gab es HIER keine Bremse gegen
|
||||
automatisiertes Durchprobieren des Zugangscodes -- im Gegensatz zum internen Team-Bereich
|
||||
(server-internal/lib/auth.js), der schon eine 5-Versuche/15-Minuten-Sperre hat. Absichtlich
|
||||
EINFACHER gehalten als dort: dieser Server hat (noch) keine Datenbank, eine Sperre im
|
||||
Arbeitsspeicher (Map, IP -> Zaehler) reicht fuer diesen Zweck voellig aus -- der einzige
|
||||
Nachteil ist, dass die Zaehlung bei einem Server-Neustart zuruecksetzt, was hier unkritisch ist
|
||||
(kein Angriff ueberlebt einen Neustart laenger als der ohnehin sehr seltene Fall, dass genau
|
||||
dann neu gestartet wird). req.ip ist dank `app.set("trust proxy", 1)` in index.js + genau einer
|
||||
vertrauten Proxy-Stufe (Caddy) nicht vom Client faelschbar -- exakt dieselbe Absicherung wie
|
||||
beim internen Bereich (siehe dortiger Kommentar vom 05.08.2026 zur CF-Connecting-IP-Luecke). */
|
||||
const GATE_MAX_ATTEMPTS = 5;
|
||||
const GATE_LOCKOUT_MS = 15 * 60 * 1000;
|
||||
const gateAttempts = new Map(); // ip -> { count, lockedUntil }
|
||||
|
||||
function gateClientKey(req) {
|
||||
return req.ip || req.socket?.remoteAddress || "unknown";
|
||||
}
|
||||
|
||||
function isGateLockedOut(key) {
|
||||
const entry = gateAttempts.get(key);
|
||||
return !!(entry && entry.lockedUntil && entry.lockedUntil > Date.now());
|
||||
}
|
||||
|
||||
function recordGateFailedAttempt(key) {
|
||||
const entry = gateAttempts.get(key) || { count: 0, lockedUntil: null };
|
||||
entry.count += 1;
|
||||
if (entry.count >= GATE_MAX_ATTEMPTS) entry.lockedUntil = Date.now() + GATE_LOCKOUT_MS;
|
||||
gateAttempts.set(key, entry);
|
||||
}
|
||||
|
||||
function clearGateAttempts(key) {
|
||||
gateAttempts.delete(key);
|
||||
}
|
||||
|
||||
export async function gateAuthHandler(req, res) {
|
||||
const body = req.body || {};
|
||||
const code = (body.code || "").trim();
|
||||
const next = typeof body.next === "string" && body.next.startsWith("/") && body.next !== "/" ? body.next : "/index.html";
|
||||
|
||||
let role = null;
|
||||
if (process.env.SITE_ACCESS_CODE_DOGI && code === process.env.SITE_ACCESS_CODE_DOGI) role = "dogi";
|
||||
else if (process.env.SITE_ACCESS_CODE_VANVAN && code === process.env.SITE_ACCESS_CODE_VANVAN) role = "vanvan";
|
||||
|
||||
if (!role) {
|
||||
const clientKey = gateClientKey(req);
|
||||
// Absichtlich DIESELBE Fehlermeldung wie bei einem falschen Code (siehe unten) -- ein Angreifer
|
||||
// soll aus der Antwort nicht ablesen koennen, ob er gerade gesperrt ist oder nur falsch getippt
|
||||
// hat (das wuerde sonst verraten, wie nah man an der Sperrschwelle dran ist).
|
||||
if (isGateLockedOut(clientKey)) {
|
||||
return res.status(401).json({ ok: false, error: "Falscher Zugangscode." });
|
||||
}
|
||||
|
||||
const body = req.body || {};
|
||||
const code = (body.code || "").trim();
|
||||
const next = isSafeNextPath(body.next) ? body.next : "/index.html";
|
||||
|
||||
let role = null;
|
||||
if (process.env.SITE_ACCESS_CODE_DOGI && timingSafeEqual(code, process.env.SITE_ACCESS_CODE_DOGI)) role = "dogi";
|
||||
else if (process.env.SITE_ACCESS_CODE_VANVAN && timingSafeEqual(code, process.env.SITE_ACCESS_CODE_VANVAN)) role = "vanvan";
|
||||
|
||||
if (!role) {
|
||||
recordGateFailedAttempt(clientKey);
|
||||
return res.status(401).json({ ok: false, error: "Falscher Zugangscode." });
|
||||
}
|
||||
clearGateAttempts(clientKey);
|
||||
|
||||
const session = await signSession(role, process.env.SITE_ACCESS_SECRET);
|
||||
// Bewusst OHNE maxAge/expires → reines Sitzungs-Cookie, verfällt beim Schließen des Browsers.
|
||||
res.cookie(COOKIE_NAME, session, { path: "/", httpOnly: true, secure: true, sameSite: "lax" });
|
||||
|
||||
Reference in New Issue
Block a user