Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 lines
2.7 KiB
TypeScript
60 lines
2.7 KiB
TypeScript
/**
|
|
* Fail-Secure-Startprüfung der sicherheitskritischen Umgebungsvariablen (F-01, Teil 3).
|
|
*
|
|
* WARUM LAZY UND MEMOISIERT — bitte NICHT zu einem Import-Zeit-`throw` "aufräumen":
|
|
* Zur Docker-Build-Zeit lädt `next build` alle Module, aber es sind KEINE echten
|
|
* Secrets gesetzt — `DATABASE_URL` ist in der builder-Stage nur ein Platzhalter
|
|
* (siehe Dockerfile) und `AUTH_SECRET` ist überhaupt nicht gesetzt. Ein `throw` auf
|
|
* Modulebene würde daher jeden Build zerlegen. Deshalb prüft `assertSecureEnv()`
|
|
* erst beim ersten TATSÄCHLICHEN Auth-Zugriff (aus `requireSession()`,
|
|
* `requirePlatformSession()` und den beiden `authorize()`-Callbacks heraus) und
|
|
* merkt sich das Ergebnis, statt bei jedem Aufruf neu zu prüfen.
|
|
*
|
|
* Ziel: Fehlende oder zu kurze Secrets führen zu einem harten Abbruch der
|
|
* Auth-Verarbeitung (fail secure) statt zu einem stillen Weiterlaufen, bei dem
|
|
* Auth.js bei Fehlkonfiguration ein Session-Objekt mit Fehlerzustand liefern
|
|
* könnte (fail open, CWE-636).
|
|
*/
|
|
|
|
// Mindestlänge für Secrets (Auth.js empfiehlt >= 32 zufällige Zeichen; 16 ist die
|
|
// harte Untergrenze, unter der wir den Start als Fehlkonfiguration werten).
|
|
const MIN_SECRET_LENGTH = 16;
|
|
|
|
let verified = false;
|
|
|
|
/**
|
|
* Prüft einmalig (memoisiert), dass die sicherheitskritischen Umgebungsvariablen
|
|
* gesetzt und plausibel sind. Wirft bei Fehlkonfiguration, sonst still.
|
|
*/
|
|
export function assertSecureEnv(): void {
|
|
if (verified) return;
|
|
|
|
// Auth.js v5 nutzt AUTH_SECRET; NEXTAUTH_SECRET wird als Fallback akzeptiert,
|
|
// damit bestehende Deployments nicht fälschlich als unsicher gewertet werden.
|
|
const authSecret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET;
|
|
if (!authSecret || authSecret.length < MIN_SECRET_LENGTH) {
|
|
throw new Error(
|
|
`Startabbruch: AUTH_SECRET fehlt oder ist zu kurz (mind. ${MIN_SECRET_LENGTH} Zeichen). ` +
|
|
"Fail-Secure statt Fail-Open — Auth-Zugriff wird abgebrochen.",
|
|
);
|
|
}
|
|
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
if (!databaseUrl || databaseUrl.length < MIN_SECRET_LENGTH) {
|
|
throw new Error("Startabbruch: DATABASE_URL fehlt oder ist ungültig.");
|
|
}
|
|
|
|
// Härtung §1: PASSWORD_PEPPER ist Pflicht (32-Byte hex = 64 Hexzeichen). Er wird
|
|
// in jeden Argon2-Hash/-Verify gemischt (src/server/password.ts) und ist NICHT aus
|
|
// AUTH_SECRET ableitbar (eigenständiges, nicht rotierbares Umgebungs-Secret).
|
|
const pepper = process.env.PASSWORD_PEPPER ?? "";
|
|
if (!/^[0-9a-fA-F]{64}$/.test(pepper)) {
|
|
throw new Error(
|
|
"Startabbruch: PASSWORD_PEPPER fehlt oder ist kein 32-Byte-Hex (64 Hexzeichen). " +
|
|
"Erzeuge ein pro-Umgebung eindeutiges Secret: openssl rand -hex 32. Fail-Secure.",
|
|
);
|
|
}
|
|
|
|
verified = true;
|
|
}
|