Patch captcha

This commit is contained in:
Esteban
2026-07-02 15:58:39 +02:00
parent 6fb78e0bbb
commit 481fbc9498
2 changed files with 339 additions and 79 deletions
+137 -47
View File
@@ -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É AUJOURDHUI',
'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É AUJOURDHUI', '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 à laction 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 dun 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 dinformations bancaires par email'],
['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse dargent 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 nas plus dessais. 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 nas plus dessais. Recharge une nouvelle série.'
: 'Ce nest 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 nas plus dessais. Recharge une nouvelle série.'
: 'Ce nest 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 nas plus dessais. 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 nest 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 nest 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;
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<link rel="stylesheet" href="./styles/captcha.css">
</head>
<body class="<?php echo $success ? 'success-mode' : ''; ?>">
@@ -351,7 +414,19 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<div class="success-burst"></div>
<div class="success-icon">✅</div>
<h1>Vérification réussie</h1>
<p>Bravo, tu as trouvé le mail frauduleux. Redirection en cours...</p>
<p>Bravo, tu as repéré le(s) mail(s) frauduleux. Redirection en cours...</p>
<?php if (!empty($foundReasons)): ?>
<div class="success-reasons">
<p class="success-reasons-title">Ce que tu as repéré :</p>
<ul>
<?php foreach ($foundReasons as $reason): ?>
<li><i class="reason-dot"></i><?php echo htmlspecialchars($reason, ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="success-loader">
<span></span>
</div>
@@ -360,7 +435,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<script>
setTimeout(function () {
window.location.href = './index.php';
}, 1800);
}, 2400);
</script>
<?php else: ?>
<form class="captcha-card" method="post" action="">
@@ -370,13 +445,14 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<div>
<h1 class="captcha-title">Vérification humaine</h1>
<p class="captcha-subtitle">
Pour continuer, cliquez sur le <strong>mail frauduleux</strong> parmi les 10 messages ci-dessous.
Sélectionne <strong>tous les emails qui te semblent frauduleux</strong> parmi les messages
ci-dessous. Certains sont plus dangereux que d'autres.
</p>
</div>
<div class="captcha-target">
<span>Vérification demandée</span>
<strong>Trouver le spam</strong>
<strong>Repérer le phishing</strong>
</div>
</div>
@@ -384,6 +460,11 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<div class="captcha-attempts">
<span>Essais restants</span>
<strong><?php echo htmlspecialchars((string) $attemptsLeft, ENT_QUOTES, 'UTF-8'); ?> / <?php echo htmlspecialchars((string) $maxAttempts, ENT_QUOTES, 'UTF-8'); ?></strong>
<div class="attempts-dots">
<?php for ($i = 0; $i < $maxAttempts; $i++): ?>
<span class="<?php echo $i < $attemptsLeft ? 'dot-filled' : 'dot-empty'; ?>"></span>
<?php endfor; ?>
</div>
</div>
<?php if ($locked): ?>
@@ -402,19 +483,21 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<span></span>
<span></span>
</div>
<div class="mailbox-label">Test anti-robot : trouvez le mail qui ressemble à une arnaque</div>
<div class="mailbox-label">Test anti-robot : cochez les mails qui ressemblent à une arnaque</div>
</div>
<div class="mail-list">
<?php foreach ($round['emails'] as $mail): ?>
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>">
<?php foreach ($round['emails'] as $index => $mail): ?>
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>" style="--mail-index: <?php echo (int) $index; ?>">
<input
type="radio"
name="selected_mail"
type="checkbox"
name="selected_mails[]"
value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>"
<?php echo $locked ? 'disabled' : 'required'; ?>
<?php echo $locked ? 'disabled' : ''; ?>
>
<span class="mail-row">
<span class="mail-check"><i class="fa-solid fa-check"></i></span>
<span class="mail-avatar">
<?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?>
</span>
@@ -447,6 +530,13 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
value="<?php echo htmlspecialchars($round['token'], ENT_QUOTES, 'UTF-8'); ?>"
>
<!-- Champ piège (honeypot) : invisible et exclu du tabulateur pour un humain,
mais souvent rempli automatiquement par les robots qui remplissent tout -->
<div class="captcha-honeypot" aria-hidden="true">
<label for="website">Laisse ce champ vide</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<div class="captcha-footer">
<p class="captcha-note">
Tu as plusieurs essais. Les faux mails utilisent souvent lurgence, les cadeaux improbables, les menaces ou les demandes de mot de passe.