Aller au contenu
Antisy

Protéger un formulaire PHP contre les robots, sans énigme

Posez une balise dans la page, lisez le champ caché antisy-token quand le formulaire arrive, et envoyez-le avec votre secret à POST /v1/siteverify depuis votre serveur : la réponse est allow ou deny. Quelques lignes de PHP et de cURL, sans bibliothèque.

État au .

Ce qu’il vous faut

  • Un site déclaré au tableau de bord d’Antisy, avec les noms d’hôte de vos pages.
  • La clé de site (32 caractères) : elle est publique et va dans la page ; le secret de vérification (64 caractères) reste sur votre serveur et n’est montré qu’une fois, à sa création.
  • Des pages servies en HTTPS.
  • PHP 8.0 ou plus avec l’extension cURL : la fonction de la documentation utilise curl_init et le type mixed.

Voir : Documentation : clés et environnements de test

Est-ce disponible aujourd’hui ?

L’accès se fait sur liste d’attente. L’API et le tableau de bord sont en service depuis le 27 octobre 2026, hébergés chez Infomaniak à Genève ; aucun client n’est encore en production.

Les exemples de ce guide suivent la version 1 de l’API, telle que la documentation la décrit. Pour les essayer sans protéger un vrai formulaire, la documentation décrit des clés de test ; une clé de test ne va jamais en production.

Voir : Documentation : clés de test · Le pilote · Rejoindre la liste d’attente

1. La page : une balise et un formulaire

Dans le <head> de la page qui porte le formulaire, la balise du script avec votre clé de site. Le formulaire est un formulaire HTML ordinaire en POST vers votre propre site : vous n’écrivez pas de JavaScript. L’exemple envoie à /contact ; adaptez l’adresse à votre fichier, par exemple /contact.php.

page.html
<script src="https://api.antisy.com/v1/i/l.js"
        data-site="VOTRE_CLE_DE_SITE" defer></script>

<form method="post" action="/contact">
  <input name="name" autocomplete="name">
  <input name="email" type="email" autocomplete="email">
  <textarea name="message"></textarea>
  <button>Envoyer</button>
</form>

À l’envoi, le script retient le formulaire le temps d’obtenir un jeton, crée le champ caché antisy-token, le remplit et relance l’envoi (6 secondes au plus). Par défaut, le script couvre les formulaires POST dont l’adresse d’envoi est celle de votre site.

Voir : Documentation : quels formulaires le script couvre

2. Le serveur : la fonction de vérification

Enregistrez la fonction de la documentation dans antisy.php, telle quelle. Elle lit le secret dans la variable d’environnement ANTISY_SECRET et rend allow ou deny.

antisy.php
<?php
/**
 * Vérifie le jeton Antisy (mode simple). Rend le verdict : "allow" ou "deny".
 *
 * - jeton vide ou absent : envoyé tel quel, Antisy décide ; jeton non vide inutilisable : "deny", sans appel ;
 * - Antisy ne répond pas (réseau, délai, 5xx) : "allow" et on journalise (fail-open) ;
 * - 429 : ANTISY_ON_RATE_LIMIT ;
 * - tout autre statut que 200 (400, 401, 413, 415...) : erreur de VOTRE appel, journalisée,
 *   tentative non vérifiée : "deny". Jamais "allow".
 */
const ANTISY_ON_RATE_LIMIT = 'allow'; // 'deny' pour un formulaire sensible : un attaquant peut provoquer un 429

function antisy_verdict(mixed $token): string
{
    if ($token === null) {
        $token = ''; // champ absent : traité comme vide
    }
    // Vide : envoyé tel quel, Antisy décide (collecte allumée : deny ; éteinte : le moteur décide).
    // Non vide : 1 à 2 048 caractères ASCII imprimables ; /D : « $ » ne tolère pas un saut de ligne final.
    if ($token !== '' && (!is_string($token) || !preg_match('/^[\x20-\x7e]{1,2048}$/D', $token))) {
        return 'deny'; // un tableau de formulaire, un jeton trop long ou piégé : ce que répondrait Antisy
    }

    $ch = curl_init('https://api.antisy.com/v1/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 3, // secondes ; au-delà, fail-open
        CURLOPT_FOLLOWLOCATION => false, // aucune redirection suivie (c'est le défaut, écrit pour qu'on ne l'allume pas) : une 3xx est une erreur de VOTRE appel
        CURLOPT_POSTFIELDS => http_build_query([
            'secret' => getenv('ANTISY_SECRET'), // secret siteverify ; jamais en dur ni côté client
            'response' => $token,
        ]),
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    // Seul cas où l'on laisse passer : Antisy ne répond pas.
    if ($body === false || $status >= 500) {
        error_log("Antisy injoignable, envoi laissé passer (fail-open): {$error} (HTTP {$status})");
        return 'allow';
    }
    if ($status === 429) {
        error_log('Antisy 429 (voie saturée): ' . ANTISY_ON_RATE_LIMIT);
        return ANTISY_ON_RATE_LIMIT;
    }
    if ($status !== 200) {
        error_log("Antisy HTTP {$status} : erreur de l'appel, à corriger ; tentative non vérifiée");
        return 'deny';
    }
    $result = json_decode($body, true);
    if (is_array($result) && is_bool($result['success'] ?? null)) {
        if ($result['success']) {
            return 'allow'; // la seule acceptation
        }
        return 'deny'; // success false : refus (le verdict vaut « deny »)
    }
    error_log('Réponse Antisy illisible, tentative non vérifiée');
    return 'deny';
}

  • Un jeton non vide doit avoir la forme d’un jeton (1 à 2 048 caractères ASCII imprimables) ; sinon la fonction rend deny sans appeler Antisy.
  • L’appel part en POST vers https://api.antisy.com/v1/siteverify, avec secret et response dans le corps, jamais dans l’adresse, et un délai de 3 secondes.
  • Aucune redirection n’est suivie : une réponse 3xx est une erreur de votre appel, jamais un laissez-passer.
  • success vaut true si et seulement si le verdict est allow : c’est la seule acceptation.

3. Le gestionnaire du formulaire

Un fichier complet, contact.php, qui refuse l’envoi sans dire pourquoi quand le verdict n’est pas allow :

contact.php
<?php
// contact.php : reçoit le formulaire de la page.
require __DIR__ . '/antisy.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit;
}

if (antisy_verdict($_POST['antisy-token'] ?? '') !== 'allow') {
    http_response_code(403);
    exit('Envoi refusé.'); // refus générique, sans dire pourquoi
}

// … enregistrer ou envoyer le message

header('Location: /merci.html', true, 303);
exit;

Le champ est lu avec une valeur vide par défaut (?? '') : si le script est bloqué, le champ est absent, et la fonction envoie un jeton vide, qu’Antisy lit comme un jeton absent.

Où mettre le secret ?

La fonction lit getenv('ANTISY_SECRET'). Posez la variable dans l’environnement du serveur web ou de PHP-FPM, par exemple dans le pool :

Configuration du pool PHP-FPM
# Variable d'environnement du serveur web (ici, un pool PHP-FPM) : le secret ne va ni dans la page ni dans le dépôt.
env[ANTISY_SECRET] = VOTRE_SECRET_DE_VERIFICATION

Un pool PHP-FPM vide l’environnement par défaut (clear_env) : la directive env[...] transmet la variable à PHP. N’écrivez le secret ni dans la page ni dans un fichier du dépôt, et ne l’affichez jamais dans une réponse ni dans un journal.

Que répond la vérification ?

La réponse est un objet JSON. success vaut true si et seulement si verdict vaut allow. Il n’y a que ces deux issues, et la réponse n’en donne jamais la raison : le refus que vous montrez au visiteur est générique lui aussi.

Un jeton est valable 60 secondes et ne se vérifie qu’une fois. Un second appel avec le même jeton est refusé.

Réponse de POST /v1/siteverify (HTTP 200)
{
  "success": true,
  "verdict": "allow",
  "flags": [],
  "error-codes": [],
  "challenge_ts": "2026-09-24T10:00:00Z",
  "hostname": "shop.example",
  "action": "contact",
  "event_id": "evt_01J7X4M2Q9K8ZP3R5T6V7W8Y9A"
}

Voir : Documentation : la requête, la réponse et les codes d’erreur

Et si Antisy ne répond pas ?

La fonction laisse passer l’envoi dans un seul cas : Antisy ne répond pas (réseau coupé, délai de 3 secondes dépassé, erreur 5xx). Une panne d’Antisy ne doit pas fermer votre formulaire.

Toute autre erreur HTTP (400, 401, 413, 415) vient de votre appel : la fonction refuse l’envoi et le journalise, jamais un laissez-passer. Un 429 est une décision qui vous appartient : la constante ANTISY_ON_RATE_LIMIT vaut allow par défaut ; mettez deny pour un formulaire sensible.

Voir : Documentation : la requête, la réponse et les codes d’erreur

Que se passe-t-il si le script est bloqué ?

Une politique CSP qui refuse le script, un bloqueur de publicité, une page en HTTP ou un visiteur sans JavaScript empêchent le script de tourner. Le formulaire part alors quand même, sans le champ antisy-token : lisez-le avec une valeur vide par défaut.

Ce qui suit dépend du réglage « Collecte du navigateur » du site. Collecte allumée, un envoi sans jeton ne passe pas ; collecte éteinte, l’absence de jeton n’est pas reprochée. Un site neuf naît avec la collecte éteinte.

Voir : Documentation : si le script ne tourne pas

Ce qu’un contrôle anti-bot ne remplace pas

Aucun contrôle anti-bot n’est parfait, et Antisy n’annonce aucun taux de détection. Il s’ajoute aux autres défenses d’un formulaire, comme la limitation du nombre d’envois ; il ne les remplace pas.

Questions fréquentes

Où placer le secret d’Antisy dans une application PHP ?

Dans une variable d’environnement du serveur, nommée ANTISY_SECRET : la fonction de la documentation la lit avec getenv. Le secret ne va jamais dans la page ni dans le dépôt ; le tableau de bord ne le montre qu’une fois, à sa création.

Que faire d’un formulaire PHP reçu sans jeton ?

Lisez le champ avec une valeur vide par défaut ($_POST['antisy-token'] ?? '') et appelez la fonction : Antisy décide. Collecte du navigateur allumée, un envoi sans jeton ne passe pas ; éteinte, le moteur décide sur le reste de l’évaluation.

Faut-il une bibliothèque PHP pour appeler Antisy ?

Non. La vérification est un seul appel HTTP en POST, que la fonction de la documentation fait avec cURL. Il n’existe pas de bibliothèque PHP d’Antisy : le kit serveur Node ne sert que ce langage.

Voir aussi : Protéger un formulaire Next.js contre les robots, sans énigme · Protéger un formulaire WordPress contre les robots, sans extension · Tous les guides · Documentation : guide pas à pas