Actualiser index.php
This commit is contained in:
parent
850a40526b
commit
8d25df77e8
271
index.php
271
index.php
@ -1,15 +1,16 @@
|
||||
<?php
|
||||
// chatbot.php — Version sécurisée production + anti prompt injection
|
||||
// index.php — Chatbot sécurisé LLaMA 2 7B via Ollama
|
||||
|
||||
session_start();
|
||||
|
||||
/* =======================
|
||||
CONFIG & ENV
|
||||
======================= */
|
||||
$env = parse_ini_file(__DIR__ . '/.env');
|
||||
define('API_KEY', $env['DEESEEK_API_KEY']);
|
||||
define('SYSTEM_PROMPT', $env['DEEPSEEK_SYSTEM_PROMPT']);
|
||||
define('CSRF_SECRET', $env['CSRF_SECRET']);
|
||||
$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É
|
||||
@ -24,9 +25,8 @@ header('X-XSS-Protection: 1; mode=block');
|
||||
if (!isset($_SESSION['csrf'])) {
|
||||
$_SESSION['csrf'] = hash_hmac('sha256', session_id(), CSRF_SECRET);
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['message_count'])) {
|
||||
$_SESSION['message_count'] = 0;
|
||||
if (!isset($_SESSION['messages_timestamps'])) {
|
||||
$_SESSION['messages_timestamps'] = [];
|
||||
}
|
||||
|
||||
/* =======================
|
||||
@ -39,58 +39,148 @@ function isAjax(): bool {
|
||||
|
||||
function sanitizeMessage(string $msg) {
|
||||
$msg = trim($msg);
|
||||
if ($msg === '' || mb_strlen($msg) > 500) {
|
||||
return false;
|
||||
}
|
||||
if ($msg === '' || mb_strlen($msg) > 500) return false;
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/* =======================
|
||||
ANTI PROMPT INJECTION
|
||||
VALIDATION CARACTERES
|
||||
======================= */
|
||||
function isPromptInjection(string $msg): bool {
|
||||
// regex solide : détecte toutes les tentatives d'instructions systèmes, "ignore", "révèle", "prompt", etc.
|
||||
$pattern = '/\b(ignore|oublie|révèle|prompt|instructions?|system|agis comme|tu es maintenant|roleplay|administrateur)\b/i';
|
||||
return preg_match($pattern, $msg) === 1;
|
||||
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;
|
||||
}
|
||||
|
||||
/* =======================
|
||||
API CALL
|
||||
VERBES D'ACTION INTERDITS
|
||||
======================= */
|
||||
function callDeepSeekAPI(string $userMessage): string {
|
||||
$payload = [
|
||||
'model' => 'deepseek-chat',
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => SYSTEM_PROMPT],
|
||||
['role' => 'user', 'content' => $userMessage]
|
||||
],
|
||||
'max_tokens' => 800,
|
||||
'temperature' => 0,
|
||||
'top_p' => 1
|
||||
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', 'CTF', 'CTFM1'
|
||||
];
|
||||
|
||||
$ch = curl_init('https://api.deepseek.com/v1/chat/completions');
|
||||
$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',
|
||||
'Authorization: Bearer ' . API_KEY
|
||||
],
|
||||
CURLOPT_TIMEOUT => 15
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_TIMEOUT => 300
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http !== 200 || !$response) {
|
||||
return 'Erreur du service.';
|
||||
// DEBUG: Log l'erreur
|
||||
error_log("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);
|
||||
return $json['choices'][0]['message']['content'] ?? 'Erreur de réponse.';
|
||||
|
||||
if (!$json || !isset($json['choices'][0]['message']['content'])) {
|
||||
error_log("JSON decode error: " . json_last_error_msg());
|
||||
return "Erreur: réponse LLaMA malformée.";
|
||||
}
|
||||
|
||||
return $json['choices'][0]['message']['content'];
|
||||
}
|
||||
|
||||
/* =======================
|
||||
@ -98,51 +188,26 @@ function callDeepSeekAPI(string $userMessage): string {
|
||||
======================= */
|
||||
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 ($_SESSION['message_count'] >= 1) {
|
||||
echo json_encode(['error' => 'LIMIT_REACHED']);
|
||||
exit;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if ($message === false) { echo json_encode(['error'=>'Message invalide']); exit; }
|
||||
|
||||
// anti prompt injection
|
||||
if (isPromptInjection($message)) {
|
||||
echo json_encode(['response' => 'Demande à Bouchra.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$_SESSION['message_count']++;
|
||||
|
||||
$reply = callDeepSeekAPI($message);
|
||||
|
||||
echo json_encode([
|
||||
'response' => $reply
|
||||
]);
|
||||
$reply = callLlamaAPI($message);
|
||||
echo json_encode(['response'=>$reply]);
|
||||
exit;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<!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>
|
||||
<title>bobentaz</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; }
|
||||
@ -160,84 +225,36 @@ 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 class="chat-header">Assistant IA (bobentaz)</div>
|
||||
|
||||
<div id="chat">
|
||||
<div class="message bot">
|
||||
<div class="message-bubble">
|
||||
Bonjour, comment puis‑je vous aider ?
|
||||
</div>
|
||||
<div class="message-bubble">Bonjour, comment puis‑je vous aider ?</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<div id="limitPopup">
|
||||
Pour plus, prends l'abonnement en demandant à Bouchra
|
||||
</div>
|
||||
<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');
|
||||
const input = document.getElementById('userInput');
|
||||
const btn = document.getElementById('sendBtn');
|
||||
const csrf = document.getElementById('csrf').value;
|
||||
const 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; // 🔒 XSS SAFE
|
||||
|
||||
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';
|
||||
input.disabled = true;
|
||||
btn.disabled = true;
|
||||
addMsg('Accès limité.', false);
|
||||
return;
|
||||
}
|
||||
|
||||
addMsg(data.response || 'Erreur.', false);
|
||||
btn.disabled = false;
|
||||
}
|
||||
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>
|
||||
</html
|
||||
Loading…
Reference in New Issue
Block a user