From 2440fbf9744fc340430db790e4846bceb37429d4 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Fri, 12 Jun 2026 10:48:37 +0200 Subject: [PATCH] fix: spam-friendly mail headers; docs: monitor.php token todo Use MTA-style Message-ID and display names on From/To to avoid SpamAssassin MSGID_RANDY and PDS_NAKED_TO_NUMERO. Add todo/12 for HTTP token gate on monitor.php (CLI cron unchanged). Co-authored-by: Cursor --- config/mail.php | 43 ++++++++++++++++++++++++++--- todo/12-monitor-php-token-check.md | 44 ++++++++++++++++++++++++++++++ todo/README.md | 1 + 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 todo/12-monitor-php-token-check.md diff --git a/config/mail.php b/config/mail.php index 6e0b8fd..6c55bb3 100644 --- a/config/mail.php +++ b/config/mail.php @@ -44,7 +44,7 @@ function sendMail(string $to, string $subject, string $body, ?string $from = nul sendCommand($socket, base64_encode($pass), 235); sendCommand($socket, 'MAIL FROM:<' . $envelopeFrom . '>', 250); - sendCommand($socket, 'RCPT TO:<' . $to . '>', 250); + sendCommand($socket, 'RCPT TO:<' . bareMailAddress($to) . '>', 250); sendCommand($socket, 'DATA', 354); $message = buildMessage($fromHeader, $to, $subject, $body); @@ -115,15 +115,50 @@ function readResponse($socket): string return trim($response); } +function bareMailAddress(string $address): string +{ + $address = trim($address); + + if (preg_match('/<([^>]+)>/', $address, $matches)) { + return trim($matches[1]); + } + + return $address; +} + +function encodeDisplayName(string $name): string +{ + if (preg_match('/[\r\n"]/', $name)) { + throw new InvalidArgumentException('Invalid display name'); + } + + if (preg_match('/^[A-Za-z0-9 !\'.\-]+$/', $name)) { + return $name; + } + + return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $name) . '"'; +} + +function formatMailAddress(string $email, string $displayName): string +{ + $email = bareMailAddress($email); + + return encodeDisplayName($displayName) . ' <' . $email . '>'; +} + function buildMessage(string $from, string $to, string $subject, string $body): string { $domain = env('DOMAIN', '0012.de'); $date = gmdate('D, d M Y H:i:s') . ' +0000'; - $messageId = sprintf('<%s@%s>', bin2hex(random_bytes(8)), $domain); + // MTA-style ID (time + pid) — avoids SpamAssassin MSGID_RANDY on random hex IDs. + $messageId = sprintf('<%s.%s@%s>', gmdate('YmdHis'), getmypid(), $domain); + + $fromHeader = formatMailAddress($from, env('SMTP_FROM_NAME', '0012 Monitoring')); + $toHeader = formatMailAddress($to, env('MAIL_TO_NAME', 'Alert Recipient')); $headers = [ - "From: {$from}", - "To: {$to}", + "From: {$fromHeader}", + "To: {$toHeader}", "Subject: {$subject}", "Date: {$date}", "Message-ID: {$messageId}", diff --git a/todo/12-monitor-php-token-check.md b/todo/12-monitor-php-token-check.md new file mode 100644 index 0000000..8a57e39 --- /dev/null +++ b/todo/12-monitor-php-token-check.md @@ -0,0 +1,44 @@ +# 12 — monitor.php: Token-Check (HTTP) + +**Priorität:** medium +**Abhängigkeiten:** 03 (monitor.php), 05 (Plesk-Cron) +**Referenz:** [`../public/monitor.php`](../public/monitor.php), [`../public/mail-test.php`](../public/mail-test.php) + +## Problem + +`monitor.php` ist per **GET** öffentlich erreichbar (`https://0012.de/monitor.php`). Im Plesk-Access-Log tauchen neben dem Cron-`curl` auch fremde Clients auf (z. B. `got` von AWS-IPs) — jeder kann Checks auslösen, JSON/Status lesen und Last erzeugen. + +Plesk-Cron kann das Skript **per CLI** ausführen (`php monitor.php`) — dort ist kein HTTP nötig. + +## Ziel + +HTTP-Aufrufe nur mit gültigem Token; CLI/Cron unverändert. + +## Checkliste + +- [ ] In `monitor.php` (oder `monitor-lib.php`): bei Web-SAPI (`php_sapi_name() !== 'cli'`) Token prüfen + - Query-Parameter `token` oder Header `X-Monitor-Token` + - Secret: bestehendes `MONITOR_INGEST_TOKEN` (wie `mail-test.php`) oder separates `MONITOR_RUN_TOKEN` + - Vergleich mit `hash_equals()` + - Bei Fehler: `403` + kurzer JSON/Text, **kein** Monitoring-Lauf +- [ ] Plesk-Cron auf **PHP-Skript (CLI)** umstellen, falls aktuell URL/Curl — siehe [`05-plesk-cron.md`](05-plesk-cron.md) +- [ ] Optional: `.htaccess` / Plesk „Verzeichnis schützen“ nur als Zusatz, nicht als einzige Absicherung +- [ ] Deploy + manueller Test: + - `GET /monitor.php` ohne Token → 403 + - `GET /monitor.php?token=…` → Lauf OK + - `php monitor.php` lokal/auf Server → Lauf OK +- [ ] Kurz in [`../MONITORING.md`](../MONITORING.md) oder [`05-plesk-cron.md`](05-plesk-cron.md) dokumentieren + +## Nicht im Scope + +- Rate-Limit / IP-Whitelist (optional später) +- Absicherung von `index.php` (öffentliche Statusseite bleibt bewusst offen) + +## Akzeptanzkriterien + +- Fremde GET-Requests ohne Token lösen keinen Monitor-Lauf aus +- Geplanter Plesk-Cron (CLI) läuft weiter zuverlässig + +## Referenz-Implementierung + +Vorbild Token-Check: [`../public/mail-test.php`](../public/mail-test.php) (Zeilen 15–22). diff --git a/todo/README.md b/todo/README.md index 7482e61..6f99925 100644 --- a/todo/README.md +++ b/todo/README.md @@ -15,6 +15,7 @@ Nummerierte Todos für den 0012.de-Webspace. Abhängigkeiten beachten. | 09 | [09-monitoring-phase3.md](09-monitoring-phase3.md) | **low** | Rest-Domains, Dashboard, Header-Checks | | 10 | [10-optional-plesk-git-composer.md](10-optional-plesk-git-composer.md) | backlog | Plesk Git / Composer evaluieren | | 11 | [11-boka-smtp-relay.md](11-boka-smtp-relay.md) | **medium** | boka-Apps über 0012.de-SMTP | +| 12 | [12-monitor-php-token-check.md](12-monitor-php-token-check.md) | **medium** | HTTP-Zugriff auf `monitor.php` per Token | ## Backlog (ohne Priorität)