mivita/app/Services/FormSpamGuard.php
Kevin Adametz 4c944aa15d Spamschutz Kontakt + Registrierung: Turnstile, Rate-Limit, Honeypot; Passwort min. 8
- Kontaktformular: keine Mail mehr an die eingegebene Adresse (wurde als
  Spam-Versender missbraucht); Anfrage nur an Kontakt-Adresse + Berater (BCC),
  Reply-To = Absender; Eingaben in der Mail escaped
- Kontakt + Registrierung: Cloudflare Turnstile statt reCAPTCHA (Schluessel
  aus dem Code in die .env), Rate-Limits, Honeypot + Mindest-Ausfuellzeit,
  Namensregel (lateinische Schrift, max. 50 Zeichen), Form Requests
- Registrierung: Partner-Nummer nur fuer aktive Berater (vorher ungeprueft)
- CRM: POST /register abgeschaltet (legte ohne Captcha Benutzer an)
- Passwort-Mindestlaenge systemweit 8 Zeichen ueber Password::defaults()
- Log abgefangener Anfragen: storage/logs/form-spam-*.log
- Doku und Protokoll: dev/2026-10-09-spam/README.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:22:02 +00:00

69 lines
1.9 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Unsichtbare Bot-Erkennung für öffentliche Formulare (Kontakt, Registrierung):
* Honeypot-Feld und Mindest-Ausfüllzeit.
*
* Erkannte Bots bekommen trotzdem die Erfolgsseite, damit sie nichts dazulernen.
*/
class FormSpamGuard
{
public const HONEYPOT_FIELD = 'form_extra';
public const TIMESTAMP_FIELD = 'form_token';
public const MIN_SECONDS = 3;
public function makeFormToken(): string
{
return encrypt(now()->timestamp);
}
/**
* Liefert den Grund, warum die Anfrage als Bot gilt, oder null.
*/
public function blockReason(Request $request): ?string
{
if (filled($request->input(self::HONEYPOT_FIELD))) {
return 'honeypot';
}
try {
$startedAt = (int) decrypt((string) $request->input(self::TIMESTAMP_FIELD));
} catch (DecryptException) {
return 'invalid_form_token';
}
if (now()->timestamp - $startedAt < self::MIN_SECONDS) {
return 'too_fast';
}
return null;
}
/**
* Protokolliert eine abgefangene Anfrage ohne Freitext-Inhalte.
*
* @param 'contact'|'register' $form
* @param array<string, mixed> $context
*/
public static function log(Request $request, string $form, string $reason, array $context = []): void
{
Log::channel('form_spam')->info('Formular abgefangen', [
'form' => $form,
'reason' => $reason,
'ip' => $request->ip(),
'host' => $request->getHost(),
'user_shop_id' => Util::getUserShop()?->id,
'email_domain' => Str::after((string) $request->input('email'), '@') ?: null,
...$context,
]);
}
}