Files
craftvia/src/server/env.ts
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
2026-09-14 11:05:39 +02:00

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;
}