Security-Hardening Runde 3: JWT, trust-proxy, weitere IDORs, Attachment-Härtung
- JWT-Algorithmus fest auf HS256 (Defense-in-Depth gegen alg-confusion)
- app.set('trust proxy', 1) – Rate-Limiter wirkt jetzt auch hinter Reverse-Proxy
- IDOR-Fix: Invoice-ECD-Endpoints + PDF-Template-Generierung (canAccessContract/ECD)
- Email-Anhang-Download: Content-Type-Safelist, SVG nie inline, nosniff, Filename-CRLF-Sanitize
- Provider/Tariff-GET-Routen: requirePermission('providers:read') (Portal-Kunden raus)
- SMTP-Header-Injection zentral in sendEmail blockiert (schützt alle Caller)
- bcrypt-Cost 10 → 12 (OWASP 2026)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,10 @@ import { encrypt, decrypt } from '../utils/encryption.js';
|
||||
import { sendEmail, SmtpCredentials } from './smtpService.js';
|
||||
import { getSystemEmailCredentials } from './emailProvider/emailProviderService.js';
|
||||
|
||||
// Bcrypt-Cost-Faktor: 12 = OWASP-empfohlen (Stand 2026), ca. 250 ms pro Hash.
|
||||
// Bestehende Hashes mit Faktor 10 bleiben gültig (bcrypt kodiert den Faktor im Hash).
|
||||
const BCRYPT_COST = 12;
|
||||
|
||||
// Mitarbeiter-Login
|
||||
export async function login(email: string, password: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
@@ -168,7 +172,7 @@ export async function customerLogin(email: string, password: string) {
|
||||
export async function setCustomerPortalPassword(customerId: number, password: string) {
|
||||
console.log('[SetPortalPassword] Setze Passwort für Kunde:', customerId);
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const hashedPassword = await bcrypt.hash(password, BCRYPT_COST);
|
||||
const encryptedPassword = encrypt(password);
|
||||
|
||||
console.log('[SetPortalPassword] Hash erstellt, Länge:', hashedPassword.length);
|
||||
@@ -211,7 +215,7 @@ export async function createUser(data: {
|
||||
roleIds: number[];
|
||||
customerId?: number;
|
||||
}) {
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
const hashedPassword = await bcrypt.hash(data.password, BCRYPT_COST);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
@@ -471,7 +475,7 @@ export async function confirmPasswordReset(token: string, newPassword: string):
|
||||
throw new Error('Der Link ist abgelaufen. Bitte fordere einen neuen an.');
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
const hash = await bcrypt.hash(newPassword, BCRYPT_COST);
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
@@ -493,7 +497,7 @@ export async function confirmPasswordReset(token: string, newPassword: string):
|
||||
throw new Error('Der Link ist abgelaufen. Bitte fordere einen neuen an.');
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
const hash = await bcrypt.hash(newPassword, BCRYPT_COST);
|
||||
await prisma.customer.update({
|
||||
where: { id: customer.id },
|
||||
data: {
|
||||
|
||||
@@ -49,6 +49,16 @@ export interface EmailLogContext {
|
||||
triggeredBy?: string; // User-Email
|
||||
}
|
||||
|
||||
// Security: zentrale CRLF-Prüfung gegen SMTP-Header-Injection.
|
||||
// Alle Felder, die als Header ausgehen (to/cc/subject/replyTo/references/from),
|
||||
// werden hier geprüft – egal ob der Caller aus cachedEmail, birthday, gdpr,
|
||||
// consent-public oder auth kommt.
|
||||
function containsCRLF(value: unknown): boolean {
|
||||
if (typeof value === 'string') return /[\r\n]/.test(value);
|
||||
if (Array.isArray(value)) return value.some(containsCRLF);
|
||||
return false;
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
export async function sendEmail(
|
||||
credentials: SmtpCredentials,
|
||||
@@ -56,6 +66,21 @@ export async function sendEmail(
|
||||
params: SendEmailParams,
|
||||
logContext?: EmailLogContext
|
||||
): Promise<SendEmailResult> {
|
||||
// Header-Injection-Guard (defensiv: Absender, Empfänger, Subject)
|
||||
if (
|
||||
containsCRLF(fromAddress) ||
|
||||
containsCRLF(params.to) ||
|
||||
containsCRLF(params.cc) ||
|
||||
containsCRLF(params.subject) ||
|
||||
containsCRLF(params.inReplyTo) ||
|
||||
containsCRLF(params.references)
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Ungültige Zeichen in E-Mail-Header-Feldern (CRLF nicht erlaubt)',
|
||||
};
|
||||
}
|
||||
|
||||
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||
const encryption = credentials.encryption ?? 'SSL';
|
||||
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||
|
||||
Reference in New Issue
Block a user