- 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>
69 lines
1.9 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|