Aller au contenu

Points d'extension

phil/spam-protection-bundle

UsedTokens

interface UsedTokens
{
    public function claim(string $nonce, int $ttl): bool;
}

Enregistre un nonce comme utilisé pendant ttl secondes et indique s'il était nouveau. Une revendication devrait être atomique : deux envois d'un même jeton au même moment ne doivent pas se faire dire tous les deux qu'il était nouveau.

Le bundle l'implémente sur un pool PSR-6 (CacheUsedTokens). PSR-6 n'a pas d'« ajout si absent » atomique : sa revendication lit, écrit un marqueur à elle, puis relit. De deux envois qui écrivent l'un après l'autre, celui dont le marqueur a été écrasé est refusé. Deux envois qui relisent chacun avant que l'autre n'écrive peuvent quand même passer tous les deux : un robot qui rejoue un jeton en une rafale parallèle serrée peut donc faire passer quelques envois ; les autres signaux et la limite de débit s'appliquent quand même à chacun. Un stockage qui doit fermer cette fenêtre implémente claim() sur une primitive atomique : une insertion dans une table avec une clé unique sur le nonce, qui échoue sur un doublon, ou Redis SET key value NX EX ttl.

SubmissionRateLimiter

interface SubmissionRateLimiter
{
    public function consume(string $scope, string $client): bool;
}

Compte un envoi et indique s'il reste dans la limite. Le bundle adapte une fabrique de limiteurs de débit de Symfony (SymfonySubmissionRateLimiter), avec la portée et le client comme clé.

L'inspecteur hors des formulaires

Un point de terminaison d'API ou un récepteur de webhooks utilise l'inspecteur directement :

$protection = $inspector->protect('newsletter');       // token, challenge, difficulty, for the client to echo

$inspection = $inspector->inspect(new Submission(
    scope: 'newsletter',
    honeypot: $payload['website_url'] ?? null,
    token: $payload['token'] ?? null,
    solution: $payload['proof'] ?? null,
    texts: [$payload['comment'] ?? ''],
    client: $clientKey->current(),
));

if ($inspection->isSpam()) {
    // refuse, or add friction — Verdicts::of($inspection) is a bot-detection Verdict
}