hardening
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Actions\Chatbot;
|
||||
use App\Jobs\LogChatInteractionJob;
|
||||
use App\Models\ChatLog;
|
||||
use App\Models\ChatSession;
|
||||
use App\Services\KnowledgeBase\PromptGuardService;
|
||||
use App\Services\KnowledgeBase\RAGService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -23,7 +24,8 @@ use Throwable;
|
||||
class AskQuestionAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RAGService $ragService
|
||||
private readonly RAGService $ragService,
|
||||
private readonly PromptGuardService $promptGuard,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,33 @@ class AskQuestionAction
|
||||
// ── Urus sesi ────────────────────────────────────────────────────
|
||||
$session = $this->resolveSession($request, $categoryId);
|
||||
|
||||
// ── Semak input sebelum hantar ke AI ─────────────────────────────
|
||||
$guard = $this->promptGuard->check($question);
|
||||
|
||||
if ($guard['blocked']) {
|
||||
LogChatInteractionJob::dispatch(
|
||||
$session->session_token,
|
||||
auth()->id(),
|
||||
$categoryId,
|
||||
$question,
|
||||
'INPUT_BLOCKED',
|
||||
[],
|
||||
[],
|
||||
'prompt-guard',
|
||||
null,
|
||||
0.0,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
return [
|
||||
'answer' => 'Maaf, saya tidak dapat memproses permintaan tersebut. Sila kemukakan soalan berkaitan perkhidmatan kami.',
|
||||
'has_answer' => false,
|
||||
'sources' => [],
|
||||
'session_token' => $session->session_token,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Jawab soalan melalui RAG ──────────────────────────────────────
|
||||
try {
|
||||
$result = $this->ragService->ask($question, $categoryId);
|
||||
|
||||
@@ -35,6 +35,7 @@ class LogChatInteractionJob implements ShouldQueue
|
||||
private readonly ?int $tokensUsed,
|
||||
private readonly float $responseTime,
|
||||
private readonly bool $hasAnswer,
|
||||
private readonly bool $isFlagged = false,
|
||||
) {
|
||||
$this->onQueue(config('knowledgebase.queue.chat_log', 'default'));
|
||||
}
|
||||
@@ -59,7 +60,7 @@ class LogChatInteractionJob implements ShouldQueue
|
||||
'tokens_used' => $this->tokensUsed,
|
||||
'response_time' => $this->responseTime,
|
||||
'has_answer' => $this->hasAnswer,
|
||||
'is_flagged' => false,
|
||||
'is_flagged' => $this->isFlagged,
|
||||
]);
|
||||
|
||||
$session->update(['last_activity_at' => now()]);
|
||||
|
||||
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ChatFeedback extends Model
|
||||
{
|
||||
protected $table = 'chat_feedbacks';
|
||||
|
||||
protected $fillable = [
|
||||
'chat_log_id',
|
||||
'user_id',
|
||||
|
||||
@@ -132,7 +132,7 @@ class AuditService
|
||||
'faq.converted_from_feedback',
|
||||
$knowledgeItem,
|
||||
[],
|
||||
['feedback_id' => $feedback->id, 'knowledge_item_id' => $knowledgeItem->id],
|
||||
['feedback_id' => $feedback?->id, 'knowledge_item_id' => $knowledgeItem->id],
|
||||
"FAQ baru '{$knowledgeItem->title}' dicipta dari feedback chat."
|
||||
);
|
||||
}
|
||||
|
||||
104
app/Services/KnowledgeBase/PromptGuardService.php
Normal file
104
app/Services/KnowledgeBase/PromptGuardService.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\KnowledgeBase;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PromptGuardService
|
||||
{
|
||||
// Soalan yang terlalu pendek atau terlalu panjang
|
||||
private const MIN_LENGTH = 3;
|
||||
private const MAX_LENGTH = 1000;
|
||||
|
||||
private array $patterns = [
|
||||
// Inject arahan baru
|
||||
'instruction_override' => [
|
||||
'/ignore (all |previous |above |prior |your |the )?instructions?/i',
|
||||
'/forget (everything|all|what you|your instructions|your rules)/i',
|
||||
'/override (your |all |previous )?instructions?/i',
|
||||
'/disregard (all |your |previous )?instructions?/i',
|
||||
'/new instruction[s]?:/i',
|
||||
],
|
||||
|
||||
// Ubah persona / roleplay
|
||||
'persona_switch' => [
|
||||
'/you are now\b/i',
|
||||
'/pretend (you are|to be|that you)/i',
|
||||
'/act as (a |an |if |though )/i',
|
||||
'/roleplay as/i',
|
||||
'/from now on (you are|you will|act|behave)/i',
|
||||
'/your (new |true |real )?role is/i',
|
||||
],
|
||||
|
||||
// Jailbreak klasik
|
||||
'known_jailbreak' => [
|
||||
'/\bDAN\b/', // Do Anything Now
|
||||
'/do anything now/i',
|
||||
'/jailbreak/i',
|
||||
'/developer mode/i',
|
||||
'/grandma (trick|exploit|jailbreak)/i',
|
||||
'/token manipulation/i',
|
||||
],
|
||||
|
||||
// Cuba dedah system prompt
|
||||
'prompt_extraction' => [
|
||||
'/what (is|are) your (system |initial |original )?(prompt|instruction)/i',
|
||||
'/show (me |us )?(your )?(system |hidden |secret |original )?prompt/i',
|
||||
'/repeat (your |the )?(system |above |initial )?prompt/i',
|
||||
'/print (your |the )?(system |above |initial )?instructions?/i',
|
||||
'/reveal (your |the )?(system |hidden |secret )?prompt/i',
|
||||
],
|
||||
|
||||
// Token injection
|
||||
'token_injection' => [
|
||||
'/<\|im_start\|>/i',
|
||||
'/<\|im_end\|>/i',
|
||||
'/\[INST\]/i',
|
||||
'/\[\/INST\]/i',
|
||||
'/<<SYS>>/i',
|
||||
'/\[SYSTEM\]/i',
|
||||
'/###\s*System/i',
|
||||
'/###\s*Human/i',
|
||||
'/###\s*Assistant/i',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Semak input user. Return array dengan status dan sebab.
|
||||
*
|
||||
* @return array{ blocked: bool, reason: string|null, category: string|null }
|
||||
*/
|
||||
public function check(string $input): array
|
||||
{
|
||||
$input = trim($input);
|
||||
|
||||
if (mb_strlen($input) < self::MIN_LENGTH) {
|
||||
return $this->blocked('Soalan terlalu pendek.', 'too_short');
|
||||
}
|
||||
|
||||
if (mb_strlen($input) > self::MAX_LENGTH) {
|
||||
return $this->blocked('Soalan melebihi had aksara.', 'too_long');
|
||||
}
|
||||
|
||||
foreach ($this->patterns as $category => $categoryPatterns) {
|
||||
foreach ($categoryPatterns as $pattern) {
|
||||
if (preg_match($pattern, $input)) {
|
||||
Log::warning('PromptGuard: input blocked', [
|
||||
'category' => $category,
|
||||
'pattern' => $pattern,
|
||||
'input' => mb_substr($input, 0, 200),
|
||||
]);
|
||||
|
||||
return $this->blocked('Input tidak dibenarkan.', $category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['blocked' => false, 'reason' => null, 'category' => null];
|
||||
}
|
||||
|
||||
private function blocked(string $reason, string $category): array
|
||||
{
|
||||
return ['blocked' => true, 'reason' => $reason, 'category' => $category];
|
||||
}
|
||||
}
|
||||
@@ -254,11 +254,14 @@ class OllamaService
|
||||
*/
|
||||
private function buildRagUserMessage(string $question, string $context): string
|
||||
{
|
||||
return "Konteks Rujukan:\n" .
|
||||
"================\n" .
|
||||
// Delimiter yang jelas membantu model bezakan context dokumen vs input user.
|
||||
// Ini mengurangkan risiko prompt injection dari kandungan dokumen.
|
||||
return "=== KONTEKS RUJUKAN (GUNA INI SAHAJA) ===\n" .
|
||||
$context . "\n" .
|
||||
"================\n\n" .
|
||||
"Soalan: " . $question;
|
||||
"=== TAMAT KONTEKS ===\n\n" .
|
||||
"=== SOALAN PENGGUNA ===\n" .
|
||||
$question . "\n" .
|
||||
"=== TAMAT SOALAN ===";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user