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:
+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));
|
||||
|
||||
Reference in New Issue
Block a user