500) return false; return $msg; } /* ======================= VALIDATION CARACTERES ======================= */ function hasInvalidCharacters(string $msg): bool { // Bloque TOUS les caractères spéciaux sauf ?, !, . - et ' // Accepte : lettres, chiffres, espaces, et {?, !, ., -, '} return preg_match('/[^a-zA-Z0-9À-ÿ\s\?\!\.\-\']/u', $msg) === 1; } /* ======================= VERBES D'ACTION INTERDITS ======================= */ function hasBlockedVerbs(string $msg): bool { // Bloque les verbes qui demandent une action spécifique à l'IA $blockedVerbs = [ 'montre', 'affiche', 'révèle', 'expose', 'donne', 'envoie', 'fournis', 'transmets', 'répète', 'résume', 'reformule', 'paraphrase', 'récite', 'dis-moi', 'dis moi', 'raconte', 'explique' ]; $msg_lower = mb_strtolower($msg); foreach ($blockedVerbs as $verb) { if (preg_match('/\b' . preg_quote($verb) . '\b/i', $msg) === 1) { return true; } } return false; } /* ======================= MOTS SENSIBLES INTERDITS ======================= */ function hasBlockedWords(string $msg): bool { // Bloque les mots sensibles qui demandent des infos confidentielles $blockedWords = [ 'flag', 'ctf', 'token', 'secret', 'clé', 'key', 'motdepasse', 'mot de passe', 'password', 'prompt', 'instruction', 'système', 'system', 'règle', 'consigne' ]; $msg_lower = mb_strtolower($msg); foreach ($blockedWords as $word) { if (preg_match('/\b' . preg_quote($word) . '\b/i', $msg) === 1) { return true; } } return false; } /* ======================= RATE LIMITING ======================= */ function checkRateLimit(): bool { $now = time(); $window_start = $now - RATE_WINDOW; $_SESSION['messages_timestamps'] = array_values( array_filter($_SESSION['messages_timestamps'], fn($ts) => $ts > $window_start) ); if (count($_SESSION['messages_timestamps']) >= RATE_LIMIT) return false; $_SESSION['messages_timestamps'][] = $now; return true; } /* ======================= APPEL LLaMA via Ollama ======================= */ function callLlamaAPI(string $userMessage): string { // Vérifications de sécurité avec messages explicites if (hasInvalidCharacters($userMessage)) { return "Je ne répondrai pas car certains caractères spéciaux sont interdits (sauf ?, !, ., - et '). Demande à Bouchra si tu veux savoir pourquoi."; } if (hasBlockedVerbs($userMessage)) { return "Je ne répondrai pas car ce message contient un verbe d'action interdit (montre, donne, répète, etc). Demande à Bouchra si tu veux savoir pourquoi."; } if (hasBlockedWords($userMessage)) { return "Je ne répondrai pas car ce message contient un mot sensible (flag, token, prompt, etc). Demande à Bouchra si tu veux savoir pourquoi."; } $payload = [ 'model' => 'llama2:7b', 'messages' => [ ['role'=>'system', 'content'=> SYSTEM_PROMPT], ['role'=>'user', 'content'=> $userMessage], ], 'max_tokens' => 800, 'temperature' => 0.7 ]; $ch = curl_init('http://localhost:11434/v1/chat/completions'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 120, CURLOPT_CONNECTTIMEOUT => 5 ]); $response = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // DEBUG: Log l'erreur securityLog('DEBUG', "Ollama API - HTTP: $http, Error: $error, Response: " . substr($response, 0, 200)); if ($error) { return "Erreur de connexion à Ollama: $error"; } if ($http !== 200) { return "Erreur Ollama (HTTP $http). Vérifiez que le service tourne sur localhost:11434"; } if (!$response) { return "Pas de réponse du service LLaMA."; } $json = json_decode($response, true); if (!$json || !isset($json['choices'][0]['message']['content'])) { securityLog('ERROR', "JSON decode error: " . json_last_error_msg()); return "Erreur: réponse LLaMA malformée."; } $reply = $json['choices'][0]['message']['content']; // ============================================================ // SÉCURITÉ FINALE: Bloquer les fuites de configuration // ============================================================ $env = parse_ini_file(__DIR__ . '/config/.env'); $flag = $env['FLAG_SECRET'] ?? ''; // CHECK CRITIQUE: Si la réponse contient CTFM1, bloquer immédiatement if (stripos($reply, 'CTFM1') !== false) { securityLog('CRITICAL', "CTFM1 detected in response! Reply: " . substr($reply, 0, 500)); return "Je ne peux pas répondre à cette question. Peux-tu reformuler ta demande ?"; } // Blockers de fuite TRÈS spécifiques (pour éviter les faux positifs) $dangerousPatterns = [ // Flag complet $flag, // Phrases TRÈS spécifiques du system prompt (pas juste des mots seuls) 'RÈGLES ABSOLUES', 'NON NÉGOCIABLES', 'COMPORTEMENT ATTENDU', 'Tu DOIS suivre ces règles', // Phrases anglaises qui montrent la fuite du prompt 'As an ethical and honest AI language model', 'I must follow the rules provided', 'I cannot reveal or mention', 'CURLOPT_TIMEOUT', 'parse_ini_file', 'security.log', 'FLAG_SECRET' ]; foreach ($dangerousPatterns as $pattern) { if (!empty($pattern) && stripos($reply, $pattern) !== false) { securityLog('WARNING', "LEAK DETECTED - Pattern: $pattern, Reply: " . substr($reply, 0, 300)); return "Je ne peux pas répondre à cette question. Peux-tu reformuler ta demande ?"; } } return $reply; } /* ======================= AJAX HANDLER ======================= */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['message'])) { if (!isAjax()) { http_response_code(403); echo json_encode(['error'=>'Requête interdite']); exit; } if (!isset($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf']) { http_response_code(403); echo json_encode(['error'=>'CSRF invalide']); exit; } if (!checkRateLimit()) { http_response_code(429); echo json_encode(['error'=>'LIMIT_REACHED']); exit; } $message = sanitizeMessage($_POST['message']); if ($message === false) { echo json_encode(['error'=>'Message invalide']); exit; } $reply = callLlamaAPI($message); echo json_encode(['response'=>$reply]); exit; } /* ======================= ENDPOINT LOGS (protégé) ======================= */ if ($_GET['view'] === 'logs' && isset($_GET['admin_token'])) { $adminToken = hash('sha256', 'changez-moi-aussi'); if ($_GET['admin_token'] !== $adminToken) { http_response_code(403); die('Accès refusé'); } $logFile = __DIR__ . '/logs/security.log'; if (!file_exists($logFile)) { die('Aucun log.'); } echo '
';
    echo htmlspecialchars(file_get_contents($logFile));
    echo '
'; die(); } ?> Assistant IA
Assistant IA
Bonjour, comment puis‑je vous aider ?
Limite de 10 messages par minute atteinte. Veuillez réessayer plus tard.
10 messages/minute