diff --git a/captcha.php b/captcha.php index c226894..db19866 100644 --- a/captcha.php +++ b/captcha.php @@ -20,6 +20,11 @@ if (isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true) { // Nombre maximum d'essais $maxAttempts = 3; +// Durée minimale (en secondes) entre l'affichage de la série et la soumission du formulaire. +// Un humain doit lire plusieurs messages avant de répondre : une soumission plus rapide +// que ça trahit presque toujours un robot qui remplit le formulaire instantanément. +$minSolveTime = 1.2; + // Liste des sujets normaux $normalSubjects = [ 'Votre rendez-vous est confirmé', @@ -150,28 +155,29 @@ $normalSenders = [ 'Service facturation' ]; -// Liste des sujets spam +// Liste des sujets spam, chacun associé à un indice (raison) qui sera révélé après succès : +// une petite touche pédagogique sur les techniques de phishing les plus courantes $spamSubjects = [ - 'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI', - 'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', - 'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', - 'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', - 'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', - 'ACTION OBLIGATOIRE DANS LES 5 MINUTES', - 'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', - 'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', - 'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', - 'VOUS ÊTES LE GRAND GAGNANT DU JOUR', - 'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', - 'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', - 'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', - 'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', - 'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', - 'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', - 'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', - 'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', - 'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', - 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT' + ['text' => 'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURD’HUI', 'reason' => 'Urgence exagérée et abus des majuscules'], + ['text' => 'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', 'reason' => 'Gain surprise, trop beau pour être vrai'], + ['text' => 'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', 'reason' => 'Appel à l’action pressant et menaçant'], + ['text' => 'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', 'reason' => 'Demande de paiement immédiat inhabituelle'], + ['text' => 'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', 'reason' => 'Un service légitime ne redemande jamais un mot de passe par email'], + ['text' => 'ACTION OBLIGATOIRE DANS LES 5 MINUTES', 'reason' => 'Délai artificiellement très court'], + ['text' => 'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', 'reason' => 'Usurpation d’un organisme bancaire'], + ['text' => 'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', 'reason' => 'Cadeau non sollicité'], + ['text' => 'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', 'reason' => 'Peur infondée utilisée pour pousser à agir vite'], + ['text' => 'VOUS ÊTES LE GRAND GAGNANT DU JOUR', 'reason' => 'Gain surprise non sollicité'], + ['text' => 'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', 'reason' => 'Fausse alerte de paiement pour voler des données bancaires'], + ['text' => 'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', 'reason' => 'Menace de fermeture de compte'], + ['text' => 'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', 'reason' => 'Offre “secrète” artificiellement exclusive'], + ['text' => 'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', 'reason' => 'Fausse urgence de session'], + ['text' => 'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', 'reason' => 'Remboursement non sollicité'], + ['text' => 'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', 'reason' => 'Compte à rebours anxiogène'], + ['text' => 'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', 'reason' => 'Menace de suppression définitive'], + ['text' => 'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', 'reason' => 'Fausse alerte de piratage'], + ['text' => 'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', 'reason' => 'Demande d’informations bancaires par email'], + ['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse d’argent contre des données personnelles'], ]; // Liste des expéditeurs spam @@ -219,6 +225,10 @@ function firstLetterSafe(string $text): string return strtoupper(substr($text, 0, 1)); } +// Construit une nouvelle série de mails pour une manche de captcha. +// Le nombre de mails frauduleux varie entre 1 et 3 : un robot ne peut donc pas se +// contenter de "toujours cocher une seule case au hasard", ce qui réduit fortement +// les chances de réussir par pure chance (voir le calcul dans la doc du projet). function buildInboxRound( array $normalSubjects, array $normalSenders, @@ -236,18 +246,20 @@ function buildInboxRound( 'subject' => $subject, 'preview' => $normalPreviews[array_rand($normalPreviews)], 'is_spam' => false, + 'reason' => null, 'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59)) ]; } $spamPool = []; - foreach ($spamSubjects as $index => $subject) { + foreach ($spamSubjects as $index => $spam) { $spamPool[] = [ 'id' => 'spam_' . $index, 'sender' => $spamSenders[array_rand($spamSenders)], - 'subject' => $subject, + 'subject' => $spam['text'], 'preview' => $spamPreviews[array_rand($spamPreviews)], 'is_spam' => true, + 'reason' => $spam['reason'], 'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59)) ]; } @@ -255,15 +267,25 @@ function buildInboxRound( shuffle($normalPool); shuffle($spamPool); - $selected = array_slice($normalPool, 0, 9); - $selected[] = $spamPool[0]; + // Entre 1 et 3 mails frauduleux dans les 10 affichés (voir commentaire de la fonction) + $spamCount = random_int(1, 3); + $selectedSpam = array_slice($spamPool, 0, $spamCount); + $selectedNormal = array_slice($normalPool, 0, 10 - $spamCount); + + $selected = array_merge($selectedSpam, $selectedNormal); shuffle($selected); + $correctIds = array_map(function ($mail) { + return $mail['id']; + }, $selectedSpam); + sort($correctIds); + return [ 'token' => bin2hex(random_bytes(16)), - 'correct_id' => $spamPool[0]['id'], + 'correct_ids' => $correctIds, 'emails' => $selected, - 'attempts_left' => $maxAttempts + 'attempts_left' => $maxAttempts, + 'started_at' => microtime(true) ]; } @@ -283,8 +305,15 @@ $round = $_SESSION['captcha_round']; $error = ''; $success = false; $locked = false; +$foundReasons = []; -if (!isset($round['emails']) || !is_array($round['emails'])) { +// Regénère une série si la structure en session est invalide ou date d'avant cette mise à +// jour (ancienne session sans correct_ids/started_at) : évite tout crash après déploiement +if ( + !isset($round['emails']) || !is_array($round['emails']) || + !isset($round['correct_ids']) || !is_array($round['correct_ids']) || + !isset($round['started_at']) +) { $_SESSION['captcha_round'] = buildInboxRound( $normalSubjects, $normalSenders, @@ -300,33 +329,66 @@ if (!isset($round['emails']) || !is_array($round['emails'])) { // Gestion de la soumission du formulaire if ($_SERVER['REQUEST_METHOD'] === 'POST') { $postedToken = $_POST['captcha_token'] ?? ''; - $selectedMail = $_POST['selected_mail'] ?? ''; + + $selectedMails = (isset($_POST['selected_mails']) && is_array($_POST['selected_mails'])) + ? array_map('strval', $_POST['selected_mails']) + : []; + sort($selectedMails); + + // Honeypot : champ invisible pour un humain (masqué en CSS), que les robots qui + // remplissent tous les champs d'un formulaire ont tendance à compléter malgré tout + $honeypot = trim($_POST['website'] ?? ''); if (!hash_equals($round['token'], $postedToken)) { $error = 'Session invalide, recharge la page.'; } elseif (($round['attempts_left'] ?? 0) <= 0) { $locked = true; $error = 'Tu n’as plus d’essais. Recharge une nouvelle série.'; - } elseif ($selectedMail !== $round['correct_id']) { + } elseif ($honeypot !== '') { + // On fait échouer la tentative sans révéler la vraie raison, pour ne pas + // donner d'indice à un robot sur la façon dont il a été détecté + $_SESSION['captcha_round']['attempts_left']--; + $round = $_SESSION['captcha_round']; + $locked = ($round['attempts_left'] ?? 0) <= 0; + $error = $locked + ? 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.' + : 'Ce n’est pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.'; + sendLog('Captcha : champ piège rempli (robot probable)'); + } elseif ((microtime(true) - (float) $round['started_at']) < $minSolveTime) { + // Une réponse aussi rapide n'est pas plausible pour un humain qui doit lire 10 mails + $_SESSION['captcha_round']['attempts_left']--; + $round = $_SESSION['captcha_round']; + $locked = ($round['attempts_left'] ?? 0) <= 0; + $error = $locked + ? 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.' + : 'Ce n’est pas la bonne sélection. Prends le temps de lire les messages.'; + sendLog('Captcha : soumission trop rapide (robot probable)'); + } elseif ($selectedMails !== $round['correct_ids']) { $_SESSION['captcha_round']['attempts_left']--; $round = $_SESSION['captcha_round']; if (($round['attempts_left'] ?? 0) <= 0) { $locked = true; $error = 'Perdu. Tu n’as plus d’essais. Recharge une nouvelle série.'; - // Log Discord : visiteur bloqué au captcha (possible robot) - sendLog("Captcha raté : plus aucun essai restant (possible robot)"); + sendLog('Captcha raté : plus aucun essai restant (possible robot)'); } else { - $error = 'Ce n’est pas le bon mail. Regarde mieux le message le plus agressif ou trop urgent.'; - // Log Discord : mauvaise réponse au captcha - sendLog("Captcha raté : mauvaise réponse (" . $round['attempts_left'] . " essai(s) restant(s))"); + $error = 'Ce n’est pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.'; + sendLog('Captcha raté : mauvaise sélection (' . $round['attempts_left'] . ' essai(s) restant(s))'); } } else { $_SESSION['captcha_valid'] = true; + + // On récupère les explications des mails trouvés pour les afficher sur + // l'écran de succès (petit bonus pédagogique) avant de vider la manche + foreach ($round['emails'] as $mail) { + if ($mail['is_spam'] && !empty($mail['reason'])) { + $foundReasons[] = $mail['reason']; + } + } + unset($_SESSION['captcha_round']); $success = true; - // Log Discord : captcha validé, le visiteur accède au site - sendLog("Captcha réussi : nouveau visiteur sur le site"); + sendLog('Captcha réussi : nouveau visiteur sur le site'); } } @@ -342,6 +404,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0; +
@@ -351,7 +414,19 @@ $attemptsLeft = $round['attempts_left'] ?? 0;Bravo, tu as trouvé le mail frauduleux. Redirection en cours...
+Bravo, tu as repéré le(s) mail(s) frauduleux. Redirection en cours...
+ + +Ce que tu as repéré :
+