170 lines
4.5 KiB
TypeScript
170 lines
4.5 KiB
TypeScript
type ConfigIssue = {
|
|
key: string;
|
|
message: string;
|
|
};
|
|
|
|
const WEAK_DEFAULT_VALUES = new Set([
|
|
"",
|
|
"calbook",
|
|
"calbook123",
|
|
"admin",
|
|
"admin123",
|
|
"passwort",
|
|
"password",
|
|
"123456",
|
|
"bitte-einen-langen-random-wert-setzen",
|
|
"bitte-einen-random-cron-secret-setzen",
|
|
"bitte-einen-random-salt-setzen",
|
|
"0123456789abcdef0123456789abcdef",
|
|
"bittesicher123!",
|
|
"change_me",
|
|
"changeme",
|
|
"replace_me",
|
|
"todo"
|
|
]);
|
|
|
|
function normalize(value: string | undefined | null) {
|
|
return (value ?? "").trim().toLowerCase();
|
|
}
|
|
|
|
function isWeakSecret(value: string | undefined | null) {
|
|
const normalized = normalize(value);
|
|
if (!normalized) return true;
|
|
if (WEAK_DEFAULT_VALUES.has(normalized)) return true;
|
|
if (normalized.startsWith("bitte-")) return true;
|
|
if (normalized.includes("random-wert-setzen")) return true;
|
|
if (normalized.includes("random-secret-setzen")) return true;
|
|
return false;
|
|
}
|
|
|
|
function validateRuntimeConfig(): ConfigIssue[] {
|
|
const issues: ConfigIssue[] = [];
|
|
|
|
const nextAuthSecret = process.env.NEXTAUTH_SECRET;
|
|
if (!nextAuthSecret || nextAuthSecret.trim().length < 32 || isWeakSecret(nextAuthSecret)) {
|
|
issues.push({
|
|
key: "NEXTAUTH_SECRET",
|
|
message: "muss gesetzt, einzigartig und mindestens 32 Zeichen lang sein."
|
|
});
|
|
}
|
|
|
|
const cronSecret = process.env.CRON_SECRET;
|
|
if (!cronSecret || cronSecret.trim().length < 24 || isWeakSecret(cronSecret)) {
|
|
issues.push({
|
|
key: "CRON_SECRET",
|
|
message: "muss gesetzt, einzigartig und mindestens 24 Zeichen lang sein."
|
|
});
|
|
}
|
|
|
|
const caldavKey = process.env.CALDAV_ENCRYPTION_KEY;
|
|
if (!caldavKey || caldavKey.trim().length < 32 || isWeakSecret(caldavKey)) {
|
|
issues.push({
|
|
key: "CALDAV_ENCRYPTION_KEY",
|
|
message: "muss gesetzt, einzigartig und mindestens 32 Zeichen lang sein."
|
|
});
|
|
}
|
|
|
|
const jitsiSalt = process.env.JITSI_ROOM_SALT;
|
|
if (!jitsiSalt || jitsiSalt.trim().length < 24 || isWeakSecret(jitsiSalt)) {
|
|
issues.push({
|
|
key: "JITSI_ROOM_SALT",
|
|
message: "muss gesetzt, einzigartig und mindestens 24 Zeichen lang sein."
|
|
});
|
|
}
|
|
|
|
const publicUrl = process.env.PUBLIC_URL;
|
|
if (publicUrl && publicUrl.trim()) {
|
|
try {
|
|
const parsed = new URL(publicUrl);
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
issues.push({
|
|
key: "PUBLIC_URL",
|
|
message: "muss mit http:// oder https:// beginnen."
|
|
});
|
|
}
|
|
} catch {
|
|
issues.push({
|
|
key: "PUBLIC_URL",
|
|
message: "ist kein gültiger URL-Wert."
|
|
});
|
|
}
|
|
}
|
|
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
if (!databaseUrl || !databaseUrl.trim()) {
|
|
issues.push({
|
|
key: "DATABASE_URL",
|
|
message: "muss gesetzt sein."
|
|
});
|
|
} else {
|
|
try {
|
|
const parsed = new URL(databaseUrl);
|
|
const dbPassword = decodeURIComponent(parsed.password ?? "");
|
|
if (isWeakSecret(dbPassword) || dbPassword.length < 12) {
|
|
issues.push({
|
|
key: "DATABASE_URL",
|
|
message: "nutzt ein schwaches Datenbank-Passwort."
|
|
});
|
|
}
|
|
} catch {
|
|
issues.push({
|
|
key: "DATABASE_URL",
|
|
message: "ist kein gültiger URL-Wert."
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function validateSeedConfig(): ConfigIssue[] {
|
|
const issues: ConfigIssue[] = [];
|
|
const adminPassword = process.env.ADMIN_PASSWORD;
|
|
|
|
if (!adminPassword || adminPassword.trim().length < 12 || isWeakSecret(adminPassword)) {
|
|
issues.push({
|
|
key: "ADMIN_PASSWORD",
|
|
message: "muss gesetzt, stark und mindestens 12 Zeichen lang sein."
|
|
});
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function formatIssues(label: string, issues: ConfigIssue[]) {
|
|
return [
|
|
`[security] ${label}`,
|
|
...issues.map((issue) => `- ${issue.key}: ${issue.message}`)
|
|
].join("\n");
|
|
}
|
|
|
|
function isProductionRuntime() {
|
|
if (process.env.NODE_ENV !== "production") return false;
|
|
if (process.env.NEXT_PHASE === "phase-production-build") return false;
|
|
return true;
|
|
}
|
|
|
|
export function assertSecureRuntimeConfig() {
|
|
if (process.env.NODE_ENV === "test") return;
|
|
|
|
const issues = validateRuntimeConfig();
|
|
if (issues.length === 0) return;
|
|
|
|
const message = formatIssues("Unsichere Runtime-Konfiguration erkannt.", issues);
|
|
if (isProductionRuntime()) {
|
|
throw new Error(message);
|
|
}
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.warn(message);
|
|
}
|
|
|
|
export function assertSecureSeedConfig() {
|
|
if (process.env.NODE_ENV === "test") return;
|
|
|
|
const issues = validateSeedConfig();
|
|
if (issues.length === 0) return;
|
|
|
|
throw new Error(formatIssues("Unsichere Seed-Konfiguration erkannt.", issues));
|
|
}
|