PDF-Assit_RAG / backend /app /rag /security.py
Param20h's picture
deploy: pure backend API with keywords fix
7c46845 unverified
Raw
History Blame Contribute Delete
4.12 kB
"""
Prompt-injection safeguards for user questions and model outputs.
"""
import json
import re
from dataclasses import dataclass
from typing import Any, Dict
PROMPT_INJECTION_PATTERNS = [
r"\bignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|prompts?)\b",
r"\bdisregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|prompts?)\b",
r"\bforget\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|prompts?)\b",
r"\breveal\s+(the\s+)?(system|developer)\s+(prompt|message|instructions?)\b",
r"\b(show|print|display|leak|dump)\s+(the\s+)?(system|developer)\s+(prompt|message|instructions?)\b",
r"\bact\s+as\s+(the\s+)?(system|developer|admin|root)\b",
r"\byou\s+are\s+now\s+(the\s+)?(system|developer|admin|root)\b",
r"\bdisable\s+(all\s+)?(rules?|safety|guardrails?|filters?|restrictions?)\b",
r"\bbypass\s+(all\s+)?(rules?|safety|guardrails?|filters?|restrictions?)\b",
r"\boverride\s+(all\s+)?(instructions?|rules?|safety|guardrails?)\b",
r"\bdo\s+not\s+(follow|obey)\s+(the\s+)?(instructions?|rules?|system)\b",
r"\bpretend\s+(to\s+be|you\s+are)\s+(the\s+)?(system|developer|admin|root)\b",
]
_COMPILED_PATTERNS = [
re.compile(pattern, flags=re.IGNORECASE) for pattern in PROMPT_INJECTION_PATTERNS
]
BLOCKED_INPUT_MESSAGE = (
"Your message appears to contain prompt-injection instructions and was blocked."
)
MALFORMED_OUTPUT_MESSAGE = (
"I could not safely parse the model response. Please try rephrasing your question."
)
@dataclass(frozen=True)
class InputClassification:
label: str
is_safe: bool
reason: str | None = None
from app.exceptions import AppException
class UnsafePromptError(AppException):
"""Raised when user input matches prompt-injection patterns."""
def __init__(self, message: str = BLOCKED_INPUT_MESSAGE):
super().__init__("UNSAFE_PROMPT", message, 400)
class OutputParserError(ValueError):
"""Raised when the LLM response does not match the required schema."""
def classify_user_input(text: str) -> InputClassification:
"""Classify a user query as safe or prompt_injection."""
normalized = " ".join((text or "").split())
for pattern in _COMPILED_PATTERNS:
if pattern.search(normalized):
return InputClassification(
label="prompt_injection",
is_safe=False,
reason=pattern.pattern,
)
return InputClassification(label="safe", is_safe=True)
def validate_user_input(text: str) -> None:
"""Raise if the supplied user query should not reach retrieval or the LLM."""
classification = classify_user_input(text)
if not classification.is_safe:
raise UnsafePromptError(BLOCKED_INPUT_MESSAGE)
def parse_agent_output(raw_output: str) -> str:
"""
Parse the agent's final answer from a strict JSON object.
The prompt requires the final answer to be:
{"answer": "..."}
"""
payload = _load_json_object(raw_output)
answer = payload.get("answer")
if not isinstance(answer, str) or not answer.strip():
raise OutputParserError("LLM output is missing a non-empty 'answer' field.")
return answer.strip()
def _load_json_object(raw_output: str) -> Dict[str, Any]:
content = (raw_output or "").strip()
if content.lower().startswith("final answer:"):
content = content.split(":", 1)[1].strip()
try:
payload = json.loads(content)
except json.JSONDecodeError:
match = re.search(r"\{.*\}", content, flags=re.DOTALL)
if not match:
raise OutputParserError("LLM output is not valid JSON.") from None
try:
payload = json.loads(match.group(0))
except json.JSONDecodeError as exc:
raise OutputParserError("LLM output JSON is malformed.") from exc
if not isinstance(payload, dict):
raise OutputParserError("LLM output must be a JSON object.")
allowed_keys = {"answer"}
if set(payload) != allowed_keys:
raise OutputParserError("LLM output must contain exactly the 'answer' field.")
return payload