ai/index.php
2026-02-01 10:48:38 +00:00

335 lines
12 KiB
PHP
Raw Permalink 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
// index.php — Chatbot sécurisé LLaMA 2 7B via Ollama
session_start();
/* =======================
CONFIG & ENV
======================= */
$env = parse_ini_file(__DIR__ . '/config/.env');
define('SYSTEM_PROMPT', $env['SYSTEM_PROMPT'] ?? '');
define('CSRF_SECRET', $env['CSRF_SECRET'] ?? 'changez-moi');
define('RATE_LIMIT', 10); // 10 messages / minute
define('RATE_WINDOW', 60); // 60 sec
/* =======================
HEADERS SÉCURITÉ
======================= */
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
/* =======================
SESSION INIT
======================= */
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = hash_hmac('sha256', session_id(), CSRF_SECRET);
}
if (!isset($_SESSION['messages_timestamps'])) {
$_SESSION['messages_timestamps'] = [];
}
/* =======================
LOGGING SÉCURITÉ
======================= */
function securityLog(string $level, string $message): void {
$logDir = __DIR__ . '/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0700, true);
}
$logFile = $logDir . '/security.log';
$timestamp = date('Y-m-d H:i:s');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN';
$logEntry = "[$timestamp] [$level] [$ip] $message\n";
file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
function isAjax(): bool {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
function sanitizeMessage(string $msg) {
$msg = trim($msg);
if ($msg === '' || mb_strlen($msg) > 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 '<pre style="background:#f0f0f0;padding:20px;font-family:monospace;max-width:900px;margin:20px auto;border:1px solid #ccc;border-radius:8px;">';
echo htmlspecialchars(file_get_contents($logFile));
echo '</pre>';
die();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Assistant IA</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { margin:0; font-family:Inter,sans-serif; background:linear-gradient(135deg,#667eea,#764ba2); height:100vh; display:flex; justify-content:center; align-items:center; }
.chat-wrapper { width:100%; max-width:900px; height:700px; background:#fff; border-radius:24px; display:flex; flex-direction:column; overflow:hidden; }
.chat-header { padding:20px; background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-weight:600; }
#chat { flex:1; padding:20px; background:#f8fafc; overflow-y:auto; }
.message { display:flex; margin-bottom:16px; }
.message.user { justify-content:flex-end; }
.message-bubble { max-width:70%; padding:12px 16px; border-radius:14px; font-size:14px; }
.user .message-bubble { background:#667eea; color:#fff; }
.bot .message-bubble { background:#fff; color:#1e293b; }
.input-area { padding:20px; border-top:1px solid #e5e7eb; }
#limitPopup { display:none; margin-bottom:10px; padding:10px; background:#fff3cd; color:#92400e; border-radius:10px; font-size:13px; text-align:center; }
form { display:flex; gap:10px; }
input { flex:1; padding:12px; border-radius:10px; border:1px solid #ccc; }
button { padding:12px 20px; border:none; border-radius:10px; background:#667eea; color:#fff; font-weight:600; cursor:pointer; }
button:disabled { opacity:.6; cursor:not-allowed; }
.rate-counter { font-size:12px; color:#666; margin-top:8px; text-align:center; }
</style>
</head>
<body>
<div class="chat-wrapper">
<div class="chat-header">Assistant IA</div>
<div id="chat">
<div class="message bot">
<div class="message-bubble">Bonjour, comment puisje vous aider ?</div>
</div>
</div>
<div class="input-area">
<div id="limitPopup">Limite de 10 messages par minute atteinte. Veuillez réessayer plus tard.</div>
<form onsubmit="sendMsg();return false;">
<input type="hidden" id="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<input id="userInput" type="text" placeholder="Votre message…" required>
<button id="sendBtn">Envoyer</button>
</form>
<div class="rate-counter">10 messages/minute</div>
</div>
</div>
<script>
const chat=document.getElementById('chat'),input=document.getElementById('userInput'),btn=document.getElementById('sendBtn'),csrf=document.getElementById('csrf').value,popup=document.getElementById('limitPopup');
function addMsg(text,isUser){const msg=document.createElement('div');msg.className='message '+(isUser?'user':'bot');const bubble=document.createElement('div');bubble.className='message-bubble';bubble.textContent=text;msg.appendChild(bubble);chat.appendChild(msg);chat.scrollTop=chat.scrollHeight;}
async function sendMsg(){const txt=input.value.trim();if(!txt)return;addMsg(txt,true);input.value='';btn.disabled=true;const res=await fetch('',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:'message='+encodeURIComponent(txt)+'&csrf='+encodeURIComponent(csrf)});const data=await res.json();if(data.error==='LIMIT_REACHED'){popup.style.display='block';addMsg('Limite atteinte. Attendez quelques secondes.',false);btn.disabled=false;return;}popup.style.display='none';addMsg(data.response||'Erreur.',false);btn.disabled=false;}
</script>
</body>
</html>