Files
Portfolio-Esteban/captcha.php
T
2026-07-03 02:02:13 +02:00

558 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once(__DIR__ . '/functions/security.php');
// Ajout des logs Discord (chemin via __DIR__ car captcha.php est à la racine du site)
require_once(__DIR__ . '/functions/sendLogs.php');
// Recharger une nouvelle série si demandé
if (isset($_GET['reload']) && $_GET['reload'] === '1') {
unset($_SESSION['captcha_round']);
header('Location: ./captcha.php');
exit;
}
// Si le captcha est déjà validé, rediriger vers l'accueil
if (isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true) {
header('Location: ./index.php');
exit;
}
// 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é',
'Photo de famille',
'Invitation à déjeuner',
'Votre facture est disponible',
'Message de votre médecin',
'Votre colis arrivera demain',
'Reçu de paiement',
'Informations sur votre abonnement',
'Rappel de rendez-vous',
'Bulletin dinformation',
'Horaire modifié',
'Demande de nouvelles',
'Anniversaire ce week-end',
'Confirmation de réservation',
'Mise à jour de votre compte',
'Message du voisin',
'Résultats danalyse disponibles',
'Programme de la semaine',
'Votre commande a bien été envoyée',
'Merci pour votre visite',
'Document joint',
'Nouvelles de la famille',
'Convocation à la réunion',
'Changement dhoraire',
'Votre facture deau',
'Votre relevé est prêt',
'Confirmation de présence',
'Infos importantes pour demain',
'Rappel amical',
'Livraison prévue aujourdhui',
'Votre réservation est confirmée',
'Message de la mairie',
'Avis de passage',
'Votre ticket est traité',
'Mise à jour de votre dossier',
'Message de l’école',
'Facture mensuelle',
'Invitation à la réunion',
'Confirmation de paiement',
'Votre commande est en route',
'Rappel de consultation',
'Nouvelle réponse reçue',
'Votre dossier a été mis à jour',
'Planning de la semaine',
'Reçu de réservation',
'Informations utiles',
'Votre assurance vous écrit',
'Message de votre pharmacien',
'Document disponible en ligne',
'Petit rappel important',
'Votre livraison est confirmée',
'Message du service client',
'Votre demande a été reçue',
'Lettre dinformation',
'Nouvelles du jour',
'Confirmation de votre présence',
'Votre justificatif est prêt',
'Reçu de transaction',
'Votre compte a été mis à jour',
'Avis dinformation',
'Votre dossier avance',
'Information concernant votre contrat',
'Message de votre banque',
'Votre échéance approche',
'Réponse à votre demande',
'Merci pour votre confiance',
'Votre pharmacie vous informe',
'Rappel de paiement',
'Votre colis a quitté lentrepôt',
'Votre abonnement continue',
'Message de votre mutuelle',
'Invitation familiale',
'Nouveau message reçu',
'Votre place est réservée',
'Votre facture du mois',
'Compte-rendu envoyé',
'Votre demande est en cours',
'Rappel pour demain',
'Réception confirmée',
'Votre transport est réservé',
'Bonne réception',
'Votre document est validé',
'Votre suivi est disponible',
'Petit message amical',
'Confirmation de livraison',
'Votre note est disponible',
'Mise à jour dinformations',
'Votre service est activé',
'Votre message a bien été transmis',
'Reçu officiel',
'Votre planning a changé',
'Votre passage est confirmé',
'Information de dernière minute',
'Votre compte-rendu médical',
'Votre inscription est validée',
'Réponse automatique',
'Votre dossier est complet',
'Nouveau document reçu',
'Votre reçu est disponible',
'Votre commande est confirmée',
'Votre rendez-vous approche',
'Dernières informations'
];
// Liste des expéditeurs normaux
$normalSenders = [
'Docteur Martin',
'Julie',
'Paul',
'Service client',
'École primaire',
'Mairie',
'Famille Dupont',
'Pharmacie du Centre',
'EDF Service',
'Voisinage',
'Transport local',
'Maison de santé',
'Claire Bernard',
'Assurance Habitation',
'Bibliothèque municipale',
'Banque familiale',
'Centre médical',
'La Poste',
'Mutuelle Santé',
'Service facturation'
];
// 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 = [
['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
$spamSenders = [
'service-urgent@compte-bloque-now.biz',
'cadeau-gratuit@super-offre-win.click',
'banque-alerte@verification-securite.top',
'colis-stop@paiement-obligatoire.help',
'admin-compte@action-immediate.world',
'support-prime@argent-rapide-fast.support',
'verification@motdepasse-danger.win',
'alerte@compte-fermeture.click'
];
// Liste des aperçus normaux
$normalPreviews = [
'Bonjour, nous vous confirmons votre rendez-vous prévu cette semaine.',
'Je vous envoie les informations demandées, bonne journée.',
'Votre document est prêt, vous pouvez le consulter.',
'Merci pour votre message, voici les détails utiles.',
'Votre commande a bien été prise en compte.',
'Petit rappel pour ne rien oublier demain.',
'Nous restons à votre disposition pour toute question.',
'Voici un message dinformation concernant votre dossier.',
'Le service vous remercie et vous souhaite une bonne journée.',
'Vous trouverez ci-joint les éléments utiles.'
];
// Liste des aperçus spam
$spamPreviews = [
'Cliquez tout de suite sinon votre compte sera fermé dans quelques minutes.',
'Paiement obligatoire immédiat pour débloquer la situation.',
'Confirmez vos données personnelles maintenant pour éviter la suppression.',
'Votre cadeau gratuit vous attend, cliquez ici sans attendre.',
'Action urgente requise, réponse dans les 5 minutes demandée.',
'Dernier avertissement officiel avant blocage définitif.'
];
function firstLetterSafe(string $text): string
{
if (function_exists('mb_substr') && function_exists('mb_strtoupper')) {
return mb_strtoupper(mb_substr($text, 0, 1, 'UTF-8'), 'UTF-8');
}
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,
array $normalPreviews,
array $spamSubjects,
array $spamSenders,
array $spamPreviews,
int $maxAttempts
): array {
$normalPool = [];
foreach ($normalSubjects as $index => $subject) {
$normalPool[] = [
'id' => 'normal_' . $index,
'sender' => $normalSenders[array_rand($normalSenders)],
'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 => $spam) {
$spamPool[] = [
'id' => 'spam_' . $index,
'sender' => $spamSenders[array_rand($spamSenders)],
'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))
];
}
shuffle($normalPool);
shuffle($spamPool);
// 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_ids' => $correctIds,
'emails' => $selected,
'attempts_left' => $maxAttempts,
'started_at' => microtime(true)
];
}
if (!isset($_SESSION['captcha_round']) || !is_array($_SESSION['captcha_round'])) {
$_SESSION['captcha_round'] = buildInboxRound(
$normalSubjects,
$normalSenders,
$normalPreviews,
$spamSubjects,
$spamSenders,
$spamPreviews,
$maxAttempts
);
}
$round = $_SESSION['captcha_round'];
$error = '';
$success = false;
$locked = false;
$foundReasons = [];
// 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,
$normalPreviews,
$spamSubjects,
$spamSenders,
$spamPreviews,
$maxAttempts
);
$round = $_SESSION['captcha_round'];
}
// Gestion de la soumission du formulaire
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postedToken = $_POST['captcha_token'] ?? '';
$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 ($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.';
sendLog('Captcha raté : plus aucun essai restant (possible robot)');
} else {
$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;
sendLog('Captcha réussi : nouveau visiteur sur le site');
}
}
$attemptsLeft = $round['attempts_left'] ?? 0;
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test anti-robot</title>
<link rel="icon" type="image/png" href="./memoji.png">
<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' : ''; ?>">
<main class="captcha-shell">
<?php if ($success): ?>
<section class="success-screen">
<div class="success-burst"></div>
<div class="success-icon">✅</div>
<h1>Vérification réussie</h1>
<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>
</section>
<script>
setTimeout(function () {
window.location.href = './index.php';
}, 2400);
</script>
<?php else: ?>
<form class="captcha-card" method="post" action="">
<div class="captcha-badge">🛡️ Test anti-robot</div>
<div class="captcha-top">
<div>
<h1 class="captcha-title">Vérification humaine</h1>
<p class="captcha-subtitle">
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>Repérer le phishing</strong>
</div>
</div>
<div class="captcha-stats">
<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): ?>
<a class="captcha-reset" href="./captcha.php?reload=1">Recharger une nouvelle série</a>
<?php endif; ?>
</div>
<?php if ($error !== ''): ?>
<div class="captcha-error"><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></div>
<?php endif; ?>
<div class="mailbox">
<div class="mailbox-toolbar">
<div class="mailbox-dots">
<span></span>
<span></span>
<span></span>
</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 $index => $mail): ?>
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>" style="--mail-index: <?php echo (int) $index; ?>">
<input
type="checkbox"
name="selected_mails[]"
value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>"
<?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>
<span class="mail-sender">
<?php echo htmlspecialchars($mail['sender'], ENT_QUOTES, 'UTF-8'); ?>
</span>
<span class="mail-content">
<span class="mail-subject">
<?php echo htmlspecialchars($mail['subject'], ENT_QUOTES, 'UTF-8'); ?>
</span>
<span class="mail-preview">
<?php echo htmlspecialchars($mail['preview'], ENT_QUOTES, 'UTF-8'); ?>
</span>
</span>
<span class="mail-time">
<?php echo htmlspecialchars($mail['time'], ENT_QUOTES, 'UTF-8'); ?>
</span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
<input
type="hidden"
name="captcha_token"
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.
</p>
<div class="captcha-actions">
<?php if ($locked): ?>
<a class="captcha-secondary-button" href="./captcha.php?reload=1">Nouvelle série</a>
<?php else: ?>
<button type="submit" class="captcha-submit">Valider ma sélection</button>
<?php endif; ?>
</div>
</div>
</form>
<?php endif; ?>
</main>
</body>
</html>