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:
@@ -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
@@ -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 & 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 & 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
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user