Spam-Schutz: Rate-Limiting, Link-Heuristik & Spam-Logging

Mehrschichtiger Formular-Schutz ohne externe Dienste, ergänzend zu Honeypot
und HMAC-Time-Trap:
- rate_limit_ok(): pro IP+Route (5/10 min), Tages-Cap (100/Tag), Token-Replay
- client_ip() / log_spam() (abgewiesene Versuche -> storage/logs/spam.log)
- Honeypot-Feld website -> company_url umbenannt (contact-/membership-form)
- Ratelimit-Hinweis in form.js
- storage/ratelimit/ (gitignored bis auf .gitkeep)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ
This commit is contained in:
2026-06-20 20:41:50 +02:00
parent 73583a896c
commit e7801ca6ee
8 changed files with 140 additions and 9 deletions

View File

@@ -23,10 +23,24 @@ $respond = static function (bool $ok, string $error = '') use ($wantsJson): neve
$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? ''));
$route = 'kontakt';
$token = $field('ft');
// --- Spam-Checks: Bots bekommen ein stilles "OK" (kein Feedback-Kanal) ---
if ($field('website') !== '' || !form_token_valid($field('ft'))) {
if ($field('company_url') !== '') {
log_spam($route, 'honeypot');
$respond(true);
}
if (!form_token_valid($token)) {
log_spam($route, 'token');
$respond(true);
}
// Rate-Limit pro IP (sichtbarer Hinweis statt still — legitime NAT-Nutzer nicht im Dunkeln lassen).
if (!rate_limit_ok('submit|' . $route . '|' . client_ip(), 5, 600)) {
log_spam($route, 'ratelimit');
$respond(false, 'ratelimit');
}
// --- Validierung ---
$name = $field('name');
@@ -51,6 +65,22 @@ if (!$valid) {
$respond(false, 'validation');
}
// --- Inhalts-/Aufkommens-Heuristiken (still abweisen, Bot-typisch) ---
if (preg_match_all('~https?://~i', $subject . ' ' . $message) > 2) {
log_spam($route, 'links');
$respond(true);
}
// Globaler Tages-Cap als Brevo-Kosten-Backstop — die Flut soll ihn nicht bemerken.
if (!rate_limit_ok('daily', 100, 86400)) {
log_spam($route, 'daily-cap');
$respond(true);
}
// Token-Mehrfachnutzung begrenzen (Replay-Schutz, großzügig für menschliches Mehrfach-Senden).
if (!rate_limit_ok('token|' . $token, 3, 86400)) {
log_spam($route, 'replay');
$respond(true);
}
// --- Versand ---
$smtp = config('smtp');

View File

@@ -26,10 +26,24 @@ $respond = static function (bool $ok, string $error = '') use ($wantsJson): neve
$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? ''));
$route = 'mitglied-werden';
$token = $field('ft');
// --- Spam-Checks: Bots bekommen ein stilles "OK" (kein Feedback-Kanal) ---
if ($field('website') !== '' || !form_token_valid($field('ft'))) {
if ($field('company_url') !== '') {
log_spam($route, 'honeypot');
$respond(true);
}
if (!form_token_valid($token)) {
log_spam($route, 'token');
$respond(true);
}
// Rate-Limit pro IP (sichtbarer Hinweis statt still — legitime NAT-Nutzer nicht im Dunkeln lassen).
if (!rate_limit_ok('submit|' . $route . '|' . client_ip(), 5, 600)) {
log_spam($route, 'ratelimit');
$respond(false, 'ratelimit');
}
// --- Eingaben ---
$firstName = $field('first_name');
@@ -67,6 +81,18 @@ if (!$valid) {
$respond(false, 'validation');
}
// --- Aufkommens-Heuristiken (still abweisen) ---
// Globaler Tages-Cap als Brevo-Kosten-Backstop — die Flut soll ihn nicht bemerken.
if (!rate_limit_ok('daily', 100, 86400)) {
log_spam($route, 'daily-cap');
$respond(true);
}
// Token-Mehrfachnutzung begrenzen (Replay-Schutz, großzügig für menschliches Mehrfach-Senden).
if (!rate_limit_ok('token|' . $token, 3, 86400)) {
log_spam($route, 'replay');
$respond(true);
}
// --- Versand ---
$smtp = config('smtp');