Sicherheits- und Stabilitaetskorrekturen nach vollstaendiger Pruefung

- Login-Sperre war umgehbar: clientKey() vertraute dem Header CF-Connecting-IP.
  Bei Cloudflare war das sicher (CF ueberschreibt ihn), auf dem eigenen Server nicht:
  der Ursprungsserver ist auch direkt unter seiner IP erreichbar, dort konnte der
  Header frei gesetzt und die 5-Versuche-Sperre komplett ausgehebelt werden
  (nachgewiesen). Jetzt req.ip hinter trust proxy.
- Absturzsicherheit: Express 4 faengt Fehler aus async-Handlern nicht ab, eine
  einzige fehlerhafte Anfrage konnte den ganzen Dienst beenden. wrap() um alle
  Handler, zentraler Fehler-Handler, unhandledRejection/uncaughtException-Netz.
- Sicherheits-Header (X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
  Permissions-Policy, HSTS) wurden bisher nur ueber die Datei _headers gesetzt,
  die auf dem eigenen Server wirkungslos ist. Jetzt im Express-Server.
- x-powered-by abgeschaltet.
- Datenschutzerklaerung/AGB: nannten Cloudflare als Hoster und eine Cloudflare-D1-
  Datenbank. Jetzt korrekt netcup (Rechenzentrum Nuernberg) als Hoster, Cloudflare
  als vorgeschaltetes CDN mit Drittlandhinweis.
This commit is contained in:
2026-08-05 22:33:31 +02:00
parent 61729d4f12
commit d6489f787a
6 changed files with 109 additions and 25 deletions
+2 -2
View File
@@ -102,8 +102,8 @@
<p><strong>8. Verfügbarkeit und Änderungen</strong><br>
Der Anbieter bemüht sich um einen möglichst unterbrechungsfreien Zugang zum Supporter-Bereich,
garantiert aber keine ununterbrochene Verfügbarkeit (u. a. abhängig von Drittanbietern wie Cloudflare
und PayPal, siehe <a href="kontakt.html">Datenschutzerklärung</a>). Einzelne Inhalte, Meilensteine
garantiert aber keine ununterbrochene Verfügbarkeit (u. a. abhängig von Drittanbietern wie netcup,
Cloudflare und PayPal, siehe <a href="kontakt.html">Datenschutzerklärung</a>). Einzelne Inhalte, Meilensteine
oder Prämien-Designs der Kollektionen können sich ändern, ohne dass dies den Kernanspruch auf Zugang
zum Supporter-Bereich berührt.</p>
+4 -2
View File
@@ -75,7 +75,9 @@
Filipe Pereira Queiroz, 3, rue Arthur Thinnes, L-3919 Mondercange, Luxemburg — <a href="mailto:[email protected]">[email protected]</a></p>
<p><strong>2. Hosting &amp; Server-Logdaten</strong><br>
Diese Website wird über Cloudflare (Cloudflare, Inc., 101 Townsend St, San Francisco, USA; für EU-Nutzer:innen ggf. über Cloudflare-Rechenzentren in der EU) bereitgestellt. Beim Aufruf der Seite verarbeitet Cloudflare technisch notwendige Daten (u. a. IP-Adresse, Zeitstempel, aufgerufene URL, User-Agent) zur Bereitstellung, Absicherung (z. B. DDoS-Schutz) und Fehlerbehebung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an sicherem, funktionsfähigem Betrieb). Näheres: <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener">cloudflare.com/privacypolicy</a>.</p>
Diese Website wird auf einem eigenen Server der netcup GmbH (Daimlerstraße 25, 76185 Karlsruhe, Deutschland) im Rechenzentrum Nürnberg betrieben; mit netcup besteht ein Vertrag zur Auftragsverarbeitung. Beim Aufruf der Seite verarbeitet der Server technisch notwendige Daten (u. a. IP-Adresse, Zeitstempel, aufgerufene URL, User-Agent) zur Bereitstellung, Absicherung und Fehlerbehebung.<br>
Dem Server vorgeschaltet ist als Content-Delivery-Network und Schutz vor Überlastungsangriffen Cloudflare (Cloudflare, Inc., 101 Townsend St, San Francisco, USA). Dabei werden dieselben technischen Daten auch von Cloudflare verarbeitet; eine Übermittlung in die USA ist dabei nicht ausgeschlossen, abgesichert über die EU-Standardvertragsklauseln bzw. das EU-US Data Privacy Framework. Näheres: <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener">cloudflare.com/privacypolicy</a>.<br>
Rechtsgrundlage für beides ist jeweils Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an sicherem, funktionsfähigem Betrieb).</p>
<p><strong>3. Cookies &amp; lokaler Speicher</strong><br>
Diese Website nutzt derzeit keine Marketing- oder Tracking-Cookies. Eingesetzt werden ausschließlich technisch notwendige Cookies bzw. Speicherungen im Browser:</p>
@@ -92,7 +94,7 @@
Zahlungen für das DogiCrew-Supporter-Abo können über PayPal abgewickelt werden (PayPal (Europe) S.à r.l. et Cie, S.C.A., 22-24 Boulevard Royal, L-2449 Luxemburg). Dabei übermittelt PayPal die zur Zahlungsabwicklung notwendigen Daten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung). Näheres: <a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full" target="_blank" rel="noopener">paypal.com/privacy</a>.</p>
<p><strong>6. Bewerbungsformular</strong><br>
Wer sich über ein Bewerbungsformular auf dieser Seite meldet, übermittelt die dort freiwillig angegebenen Daten (z. B. Name/Spitzname, Kontaktmöglichkeit, Nachricht). Diese werden ausschließlich zur Bearbeitung der Bewerbung genutzt und in einer Cloudflare-D1-Datenbank gespeichert, Zugriff haben nur berechtigte Teammitglieder. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b bzw. f DSGVO.</p>
Wer sich über ein Bewerbungsformular auf dieser Seite meldet, übermittelt die dort freiwillig angegebenen Daten (z. B. Name/Spitzname, Kontaktmöglichkeit, Nachricht). Diese werden ausschließlich zur Bearbeitung der Bewerbung genutzt und in einer Datenbank auf dem eigenen Server (netcup, Rechenzentrum Nürnberg, siehe Ziffer 2) gespeichert, Zugriff haben nur berechtigte Teammitglieder. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b bzw. f DSGVO.</p>
<p><strong>7. DogiCrew-/Supporter-Registrierung</strong><br>
Bei der Registrierung als Supporter:in werden Name/Anzeigename, TikTok-Benutzername, E-Mail-Adresse, ein Passwort (nur als Hash gespeichert, im Klartext nicht einsehbar) sowie Abo-/Zahlungsstatus gespeichert, um den Zugang zum Supporter-Bereich zu verwalten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung). Ein eigenständiges Löschen des Kontos ist im Supporter-Bereich jederzeit selbst möglich. Beim Abschluss des kostenpflichtigen Abos wird zusätzlich die ausdrückliche Zustimmung zum sofortigen Leistungsbeginn samt Erlöschen des Widerrufsrechts (siehe <a href="agb.html">AGB</a>, Ziffer 6) mit Zeitstempel gespeichert, um dies im Streitfall nachweisen zu können.</p>
+58 -18
View File
@@ -66,6 +66,14 @@ initDb();
const PORT = Number(process.env.PORT || 4200);
const app = express();
app.set("trust proxy", 1);
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
next();
});
app.use(cors());
app.use(
@@ -81,45 +89,54 @@ app.use(
})
);
/* ---------- Absturzsicherheit ----------
Express 4 fängt Fehler aus async-Handlern NICHT selbst ab: eine abgelehnte Promise wird zu einer
"unhandled rejection", und Node beendet daraufhin den ganzen Prozess — im Cloudflare Worker war
ein Fehler dagegen immer auf die eine betroffene Anfrage begrenzt. Ohne die folgende Absicherung
könnte z.B. eine Anfrage mit fehlender ID (better-sqlite3 wirft bei `undefined` als Parameter)
den kompletten internen Bereich offline nehmen. `wrap()` leitet jeden Fehler stattdessen an den
Fehler-Handler am Ende der Datei weiter. */
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
/* ---------- Middlewares: interne Team-Session bzw. Supporter-Session ---------- */
async function requireTeamSession(req, res, next) {
const requireTeamSession = wrap(async (req, res, next) => {
const session = await requireSession(req);
if (!session) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut anmelden." }, 401);
req.session = session;
next();
}
});
async function requireSupporterAuth(req, res, next) {
const requireSupporterAuth = wrap(async (req, res, next) => {
const supporter = await requireSupporterSession(req);
if (!supporter) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut einloggen." }, 401);
req.supporter = supporter;
next();
}
});
/** Reicht die injizierte session/supporter als drittes Argument an den Handler weiter (wie im Cloudflare-Original). */
const withSession = (handler) => (req, res) => handler(req, res, req.session);
const withSupporter = (handler) => (req, res) => handler(req, res, req.supporter);
const withSession = (handler) => wrap((req, res) => handler(req, res, req.session));
const withSupporter = (handler) => wrap((req, res) => handler(req, res, req.supporter));
/* ---------- Öffentliche Routen (keine Session) ---------- */
app.get("/health", (req, res) => json(res, { ok: true, service: "dogfather-universe-interner-bereich" }));
app.post("/submit", submitApplication);
app.get("/live-status", handleLiveStatusGet);
app.post("/auth/login", login);
app.post("/submit", wrap(submitApplication));
app.get("/live-status", wrap(handleLiveStatusGet));
app.post("/auth/login", wrap(login));
app.post("/supporter/register", registerSupporter);
app.post("/supporter/verify-email", verifySupporterEmail);
app.post("/supporter/login-request", requestSupporterLogin);
app.post("/supporter/verify-login", verifySupporterLogin);
app.post("/supporter/google-login", googleLoginSupporter);
app.get("/supporter/public-config", getPublicConfig);
app.post("/supporter/register", wrap(registerSupporter));
app.post("/supporter/verify-email", wrap(verifySupporterEmail));
app.post("/supporter/login-request", wrap(requestSupporterLogin));
app.post("/supporter/verify-login", wrap(verifySupporterLogin));
app.post("/supporter/google-login", wrap(googleLoginSupporter));
app.get("/supporter/public-config", wrap(getPublicConfig));
// PayPal ruft diese Adresse direkt auf — eigene Signaturprüfung statt Session (siehe lib/paypal.js).
app.post("/supporter/paypal-webhook", handlePaypalWebhook);
app.post("/supporter/paypal-webhook", wrap(handlePaypalWebhook));
/* ---------- Team-Bereich (interne Session, requireTeamSession) ---------- */
app.post("/auth/logout", requireTeamSession, logout);
app.post("/auth/logout", requireTeamSession, wrap(logout));
app.post("/admin/owner/set-code", requireTeamSession, withSession(setOwnerCode));
app.post("/live-status/set", requireTeamSession, withSession(handleLiveStatusSet));
@@ -170,7 +187,7 @@ app.post("/admin/supporter/collections/upsert", requireTeamSession, withSession(
/* ---------- Öffentlicher Supporter-Bereich (eigene Session, requireSupporterAuth) ---------- */
app.post("/supporter/logout", requireSupporterAuth, logoutSupporter);
app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter));
app.post("/supporter/me", requireSupporterAuth, withSupporter(getSupporterMe));
app.post("/supporter/update-profile", requireSupporterAuth, withSupporter(updateSupporterProfile));
app.post("/supporter/request-email-change", requireSupporterAuth, withSupporter(requestSupporterEmailChange));
@@ -183,6 +200,29 @@ app.post("/supporter/paypal/cancel", requireSupporterAuth, withSupporter(cancelP
app.use((req, res) => json(res, { ok: false, error: "Not found." }, 404));
/* ---------- Fehler-Handler (muss NACH allen Routen stehen) ----------
Fängt alles ab, was in einem Handler schiefgeht, und antwortet mit sauberem JSON statt einer
HTML-Fehlerseite mit Stacktrace. Ersetzt gleichzeitig die try/catch-Behandlung für kaputtes
JSON, die im Cloudflare-Original noch in jeder Route einzeln stand. */
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
if (err?.type === "entity.parse.failed") {
return json(res, { ok: false, error: "Ungültiges JSON." }, 400);
}
if (err?.type === "entity.too.large") {
return json(res, { ok: false, error: "Die Anfrage ist zu groß." }, 413);
}
console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
if (res.headersSent) return;
return json(res, { ok: false, error: "Interner Fehler." }, 500);
});
/* Letztes Netz: Ein unbeaufsichtigter Fehler darf den Dienst nicht beenden. Ohne diese beiden
Zeilen würde Node den Prozess bei einer unbehandelten Promise-Ablehnung sofort beenden und der
gesamte interne Bereich (inkl. Zahlungsverarbeitung) wäre bis zum systemd-Neustart offline. */
process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund));
process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler));
/* ---------- Cron: läuft jede Minute, hält den Live-Punkt aktuell (ersetzt Cloudflares scheduled()) ---------- */
cron.schedule("* * * * *", () => {
refreshLiveFromTikTok().catch((err) => console.error("[live-status] Fehler beim Aktualisieren:", err?.message || err));
+14 -1
View File
@@ -12,8 +12,21 @@ const LOCKOUT_MINUTES = 15;
const INACTIVITY_MS = 24 * 60 * 60 * 1000;
const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
/* Schlüssel für die Fehlversuchs-Zählung (Login-Sperre).
SICHERHEITSKORREKTUR 05.08.2026: Vorher stand hier `req.headers["cf-connecting-ip"] || req.ip`.
Bei Cloudflare war das sicher, weil Cloudflare diesen Header selbst setzt und einen vom Client
mitgeschickten überschreibt. Auf dem eigenen Server gilt das NICHT mehr: der Ursprungsserver ist
(nachgewiesen am 05.08.) auch direkt unter seiner IP erreichbar, also an Cloudflare vorbei — ein
Angreifer konnte dort bei jedem Versuch einen anderen `CF-Connecting-IP`-Wert mitschicken und so
die 5-Versuche-Sperre komplett aushebeln (unbegrenztes Durchprobieren des Owner-Codes).
`req.ip` ist hier nicht fälschbar: `app.set("trust proxy", 1)` in index.js lässt Express genau
eine Proxy-Ebene (den lokalen Caddy) vertrauen, und Caddy hängt die echte Verbindungs-IP hinten
an `X-Forwarded-For` an — ein vom Client vorgetäuschter Wert landet weiter vorne und wird
ignoriert. */
export function clientKey(req) {
return req.headers["cf-connecting-ip"] || req.ip || "unknown";
return req.ip || req.socket?.remoteAddress || "unknown";
}
function timingSafeEqual(a, b) {
+4 -2
View File
@@ -80,7 +80,9 @@ export function gateMiddleware(req, res, next) {
if (!codesKonfiguriert) return next();
if (pfad === "/gate-auth" && req.method === "POST") {
return gateAuthHandler(req, res);
// .catch(next) ergänzt 05.08.2026: ohne das würde ein Fehler beim Signieren zu einer
// unbehandelten Promise-Ablehnung und damit zum Absturz des ganzen Servers führen.
return Promise.resolve(gateAuthHandler(req, res)).catch(next);
}
const sessionToken = req.cookies?.[COOKIE_NAME];
@@ -101,5 +103,5 @@ export function gateMiddleware(req, res, next) {
const nextParam = pfad === "/" ? "/index.html" : pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : "");
return res.redirect(302, `/gate.html?next=${encodeURIComponent(nextParam)}`);
})();
})().catch(next); // siehe oben: verhindert Serverabsturz bei einem Fehler in der Prüfung
}
+27
View File
@@ -18,6 +18,20 @@ const PORT = Number(process.env.PORT || 4100);
const app = express();
app.set("trust proxy", 1);
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack
/* Sicherheits-Header, ergänzt 05.08.2026. Vorher standen diese in der Datei `_headers` — die
funktioniert aber NUR bei Cloudflare Pages/Netlify und wurde auf dem eigenen Server schlicht
ignoriert, d.h. die Seite lief seit dem Umzug komplett ohne diese Schutzmaßnahmen (nachgeprüft
an den Live-Antwortheadern). */
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "SAMEORIGIN");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=()");
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
next();
});
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
@@ -31,6 +45,19 @@ app.use((req, res) => {
});
});
/* Fehler-Handler (muss NACH allen Routen stehen), ergänzt 05.08.2026: antwortet mit einer
schlichten Meldung statt einer Express-Standardseite mit komplettem Stacktrace und Dateipfaden. */
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
if (res.headersSent) return;
res.status(500).send("Es ist ein Fehler aufgetreten. Bitte später erneut versuchen.");
});
/* Letztes Netz: Ein unbehandelter Fehler darf die Website nicht offline nehmen. */
process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund));
process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler));
app.listen(PORT, "127.0.0.1", () => {
console.log(`DogFather Universe Server läuft auf http://127.0.0.1:${PORT}`);
});