Pentest R97: Attachment-Validierung im Send-Handler
R97.1 LOW: malformed content (null, fehlend, true, "") landete mit rohem Buffer.from()-Fehlertext in der Response; "" liess sogar 0-Byte-Anhänge durch. R97.2 INFO: keine App-Level-Caps für Größe/Anzahl – die im Frontend dokumentierten 10/25 MB hingen am bodyParser. Fix: validateAttachments() läuft VOR sendEmail() im Controller: - max 25 Anhänge - filename non-empty String, content non-empty Base64, optionaler contentType als String - 10 MB pro Datei, 25 MB total (Größen-Schätzung über base64-Länge, kein Buffer.from während Validierung) Harte 400 mit klarer Meldung. Sanity-Test 18/18 grün. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -392,6 +392,87 @@ function hasCRLF(value: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pentest 97.1 + 97.2 (LOW/INFO, 2026-06-21): Attachment-Validierung
|
||||
// vor `Buffer.from(...)` damit malformed Content (null, true, leerer
|
||||
// String, falsches Base64) nicht erst tief im SMTP-Service kracht und
|
||||
// dort die rohe Node.js-Fehlermeldung in der Response landet. Außerdem
|
||||
// explizite App-Level-Caps (10 MB pro Datei, 25 MB total, 25 Anhänge),
|
||||
// damit die Frontend-Doku auch ohne bodyParser-Heroics gilt.
|
||||
const MAX_ATTACHMENT_COUNT = 25;
|
||||
const MAX_PER_FILE_BYTES = 10 * 1024 * 1024;
|
||||
const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
|
||||
// Standard-Base64-Alphabet inkl. optionalem `=`-Padding (kein URL-safe).
|
||||
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
|
||||
interface AttachmentValidationError {
|
||||
status: 400;
|
||||
error: string;
|
||||
}
|
||||
|
||||
function validateAttachments(
|
||||
attachments: unknown,
|
||||
): { ok: true } | AttachmentValidationError {
|
||||
if (attachments === undefined) return { ok: true };
|
||||
if (!Array.isArray(attachments)) {
|
||||
return { status: 400, error: 'attachments muss ein Array sein.' };
|
||||
}
|
||||
if (attachments.length > MAX_ATTACHMENT_COUNT) {
|
||||
return {
|
||||
status: 400,
|
||||
error: `Maximal ${MAX_ATTACHMENT_COUNT} Anhänge pro E-Mail erlaubt (${attachments.length} übermittelt).`,
|
||||
};
|
||||
}
|
||||
let totalBytes = 0;
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
const a = attachments[i];
|
||||
const label = `Anhang ${i + 1}`;
|
||||
if (!a || typeof a !== 'object') {
|
||||
return { status: 400, error: `${label} hat das falsche Format.` };
|
||||
}
|
||||
const filename = (a as Record<string, unknown>).filename;
|
||||
const content = (a as Record<string, unknown>).content;
|
||||
const contentType = (a as Record<string, unknown>).contentType;
|
||||
if (typeof filename !== 'string' || filename.trim() === '') {
|
||||
return { status: 400, error: `${label} hat keinen Dateinamen.` };
|
||||
}
|
||||
if (typeof content !== 'string' || content.length === 0) {
|
||||
return {
|
||||
status: 400,
|
||||
error: `Anhang "${filename}" hat keinen Inhalt (content muss ein nicht-leerer Base64-String sein).`,
|
||||
};
|
||||
}
|
||||
if (!BASE64_RE.test(content)) {
|
||||
return {
|
||||
status: 400,
|
||||
error: `Anhang "${filename}" ist kein gültiger Base64-String.`,
|
||||
};
|
||||
}
|
||||
if (contentType !== undefined && typeof contentType !== 'string') {
|
||||
return {
|
||||
status: 400,
|
||||
error: `Anhang "${filename}" hat ein ungültiges contentType-Feld.`,
|
||||
};
|
||||
}
|
||||
// Größen-Abschätzung anhand der Base64-Länge: jedes 4-Zeichen-Quartett
|
||||
// dekodiert zu max 3 Bytes. So vermeiden wir Buffer.from() schon hier.
|
||||
const approxBytes = Math.ceil(content.length * 0.75);
|
||||
if (approxBytes > MAX_PER_FILE_BYTES) {
|
||||
return {
|
||||
status: 400,
|
||||
error: `Anhang "${filename}" überschreitet die Maximalgröße von ${MAX_PER_FILE_BYTES / 1024 / 1024} MB.`,
|
||||
};
|
||||
}
|
||||
totalBytes += approxBytes;
|
||||
if (totalBytes > MAX_TOTAL_BYTES) {
|
||||
return {
|
||||
status: 400,
|
||||
error: `Gesamtgröße aller Anhänge überschreitet ${MAX_TOTAL_BYTES / 1024 / 1024} MB.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
export async function sendEmailFromAccount(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
@@ -408,6 +489,18 @@ export async function sendEmailFromAccount(req: AuthRequest, res: Response): Pro
|
||||
return;
|
||||
}
|
||||
|
||||
// Pentest 97.1 + 97.2: Attachment-Validierung vor Buffer.from()
|
||||
// (Format, Größe, Anzahl) – sonst leakte der rohe Node.js-Fehler
|
||||
// in die Response und Limits waren nur Frontend-Doku.
|
||||
const attachmentCheck = validateAttachments(attachments);
|
||||
if (!('ok' in attachmentCheck)) {
|
||||
res.status(attachmentCheck.status).json({
|
||||
success: false,
|
||||
error: attachmentCheck.error,
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// StressfreiEmail laden
|
||||
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(stressfreiEmailId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user