From d57f334229f92fdda39cd2e8315dcd0d546bd9fe Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Wed, 8 Jul 2026 07:41:44 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#117:=20E-Mail-Header-/SMTP-Command-Injec?= =?UTF-8?q?tion=20=C3=BCber=20Empf=C3=A4nger-/CC-Adressen=20verhindert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changes.md | 10 ++++ src/Controllers/CustomerController.php | 8 +++ src/Services/SmtpMailer.php | 6 +++ src/Views/customers/form.php | 10 ++-- tests/50_EmailHeaderInjectionTest.php | 74 ++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/50_EmailHeaderInjectionTest.php diff --git a/Changes.md b/Changes.md index 74f4cc1..0ae2aa4 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,16 @@ --- +## 2026-07-08 – Fix: SMTP-/E-Mail-Header-Injection über ungeprüfte Empfänger-/CC-Adressen (Issue #117) + +- **Kritisch:** Empfänger- und CC-Adressen (Rechnungsversand, Test-Mail, Kundenstammdaten) wurden nur `trim()`t, aber nie auf gültiges Format oder enthaltene `\r`/`\n` geprüft. Da `SmtpMailer` das SMTP-Protokoll über einen rohen Socket implementiert, konnte eine Adresse mit eingebettetem CRLF zusätzliche `RCPT TO:`-Kommandos oder Mail-Header einschleusen (CWE-93, SMTP-/Header-Injection). +- **`src/Services/SmtpMailer.php`:** `send()` validiert jetzt jede Empfängeradresse mit `filter_var(..., FILTER_VALIDATE_EMAIL)`, bevor überhaupt eine SMTP-Verbindung aufgebaut wird; ungültige Adressen (inkl. CRLF-Payloads) werfen eine `RuntimeException`. Da alle Versandwege (`EmailService::sendInvoice()`, `sendTestMail()`) durch diese einzige Methode laufen, ist die Prüfung zentral wirksam. +- **`src/Controllers/CustomerController.php`:** Formularvalidierung prüft `email`, `email_cc1`, `email_cc2` zusätzlich mit `filter_var(FILTER_VALIDATE_EMAIL)`, damit gar nicht erst ungültige Adressen in der Kundendatenbank landen. +- **`src/Views/customers/form.php`:** Fehleranzeige für die drei E-Mail-Felder ergänzt. +- **`tests/50_EmailHeaderInjectionTest.php`:** Neuer Test prüft, dass CRLF-Injection-Payloads und ungültige Adressformate von `SmtpMailer::send()` abgelehnt werden. + +--- + ## 2026-07-08 – Fix: Unauthentifizierter Direktzugriff auf Belege/Rechnungs-PDFs (Issue #116) - **Kritisch:** Hochgeladene Belege und Rechnungs-PDFs lagen unter `public/static/uploads/...` im Web-Root und wurden von Apache direkt ausgeliefert, bevor der Request `index.php`/`Auth::require()` erreichte – ein Angreifer konnte Belege ohne Anmeldung per URL abrufen. diff --git a/src/Controllers/CustomerController.php b/src/Controllers/CustomerController.php index 1399ec1..af0cb51 100644 --- a/src/Controllers/CustomerController.php +++ b/src/Controllers/CustomerController.php @@ -139,6 +139,14 @@ class CustomerController extends BaseController if (empty($data['strasse'])) $errors['strasse'] = 'Pflichtfeld'; if (empty($data['plz'])) $errors['plz'] = 'Pflichtfeld'; if (empty($data['ort'])) $errors['ort'] = 'Pflichtfeld'; + + foreach (['email', 'email_cc1', 'email_cc2'] as $field) { + $value = trim($data[$field] ?? ''); + if ($value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) === false) { + $errors[$field] = 'Ungültige E-Mail-Adresse'; + } + } + return $errors; } } diff --git a/src/Services/SmtpMailer.php b/src/Services/SmtpMailer.php index 494b136..b54b493 100644 --- a/src/Services/SmtpMailer.php +++ b/src/Services/SmtpMailer.php @@ -39,6 +39,12 @@ class SmtpMailer ?string $attachContent = null, ?string $attachName = null, ): void { + foreach ($to as $addr) { + if (filter_var($addr, FILTER_VALIDATE_EMAIL) === false) { + throw new \RuntimeException('Ungültige E-Mail-Adresse: ' . $addr); + } + } + $this->connect(); $this->authenticate(); $this->sendMessage($to, $subject, $body, $attachContent, $attachName); diff --git a/src/Views/customers/form.php b/src/Views/customers/form.php index 1a860f3..c14aa4b 100644 --- a/src/Views/customers/form.php +++ b/src/Views/customers/form.php @@ -52,7 +52,9 @@
- + +
@@ -64,12 +66,14 @@
+ placeholder="cc1@example.com" class=""> +
+ placeholder="cc2@example.com" class=""> +

diff --git a/tests/50_EmailHeaderInjectionTest.php b/tests/50_EmailHeaderInjectionTest.php new file mode 100644 index 0000000..80c4329 --- /dev/null +++ b/tests/50_EmailHeaderInjectionTest.php @@ -0,0 +1,74 @@ +send( + ["opfer@example.com\r\nRCPT TO:"], + 'Betreff', + 'Text', + ); + $this->assert(false, 'CRLF-Adresse wirft RuntimeException'); + } catch (\RuntimeException $e) { + $this->assert(true, 'CRLF-Adresse wirft RuntimeException'); + } + + // Zusätzlicher Header via Newline im To-Feld wird abgelehnt + try { + $mailer->send( + ["opfer@example.com\nBcc: angreifer@example.com"], + 'Betreff', + 'Text', + ); + $this->assert(false, 'Header-Injection-Adresse wirft RuntimeException'); + } catch (\RuntimeException $e) { + $this->assert(true, 'Header-Injection-Adresse wirft RuntimeException'); + } + + // Grob fehlerhaftes Format wird abgelehnt + try { + $mailer->send(['keine-email-adresse'], 'Betreff', 'Text'); + $this->assert(false, 'Ungültiges Format wirft RuntimeException'); + } catch (\RuntimeException $e) { + $this->assert(true, 'Ungültiges Format wirft RuntimeException'); + } + + // Kunde: CC-Adressen mit CRLF/ungültigem Format werden vom Model nicht blind übernommen, + // sondern müssen weiterhin filter_var-tauglich sein, bevor sie je in den Mailer gelangen + $this->assert( + filter_var("opfer@example.com\r\nRCPT TO:", FILTER_VALIDATE_EMAIL) === false, + 'filter_var erkennt CRLF-Payload als ungültig' + ); + + // Regressionsschutz: gültige Adressen funktionieren weiterhin (Verbindungsaufbau schlägt + // erwartungsgemäß fehl, da kein echter SMTP-Server erreichbar ist – Validierung selbst greift nicht) + try { + $mailer->send(['gueltig@example.com'], 'Betreff', 'Text'); + $this->assert(false, 'Verbindung zu Dummy-Host sollte fehlschlagen'); + } catch (\RuntimeException $e) { + $this->assert( + str_contains($e->getMessage(), 'SMTP-Verbindung') || !str_contains($e->getMessage(), 'Ungültige E-Mail-Adresse'), + 'Gültige Adresse wird nicht durch Validierung blockiert (Fehler: ' . $e->getMessage() . ')' + ); + } + } +}