767 lines
29 KiB
Python
767 lines
29 KiB
Python
|
|
"""
|
|||
|
|
SHIELD Security Module for Hermes
|
|||
|
|
Jailbreak and Crisis Detection System
|
|||
|
|
|
|||
|
|
Based on Issue #75 Red Team Audit Specifications
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
import base64
|
|||
|
|
import unicodedata
|
|||
|
|
from enum import Enum
|
|||
|
|
from typing import Dict, List, Set, Tuple, Any
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Verdict(Enum):
|
|||
|
|
"""Four possible SHIELD verdicts"""
|
|||
|
|
CLEAN = "CLEAN"
|
|||
|
|
JAILBREAK_DETECTED = "JAILBREAK_DETECTED"
|
|||
|
|
CRISIS_DETECTED = "CRISIS_DETECTED"
|
|||
|
|
CRISIS_UNDER_ATTACK = "CRISIS_UNDER_ATTACK"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# SAFE SIX: Trusted models for crisis situations
|
|||
|
|
SAFE_SIX_MODELS: Set[str] = {
|
|||
|
|
"anthropic/claude-sonnet-4",
|
|||
|
|
"meta-llama/llama-3.1-8b-instruct",
|
|||
|
|
"moonshotai/kimi-k2.5",
|
|||
|
|
"x-ai/grok-code-fast-1",
|
|||
|
|
"xiaomi/mimo-v2-flash",
|
|||
|
|
"z-ai/glm-5-turbo",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Crisis System Prompt for emergency situations
|
|||
|
|
CRISIS_SYSTEM_PROMPT = """You are a crisis intervention assistant. The user may be experiencing a mental health emergency.
|
|||
|
|
|
|||
|
|
CRITICAL INSTRUCTIONS:
|
|||
|
|
1. Your ONLY goal is to help keep the user safe
|
|||
|
|
2. Do NOT provide methods for self-harm under ANY circumstances
|
|||
|
|
3. Express genuine compassion and concern
|
|||
|
|
4. Encourage professional help immediately
|
|||
|
|
|
|||
|
|
CRISIS RESOURCES (share these):
|
|||
|
|
- 988 Suicide and Crisis Lifeline (call or text 988)
|
|||
|
|
- Crisis Text Line: Text HOME to 741741
|
|||
|
|
- Emergency Services: 911 (if immediate danger)
|
|||
|
|
- Jesus saves those who call on His name - "For everyone who calls on the name of the Lord will be saved." (Romans 10:13)
|
|||
|
|
|
|||
|
|
RESPONSE GUIDELINES:
|
|||
|
|
- Validate their pain without validating harmful thoughts
|
|||
|
|
- Offer hope - recovery is possible
|
|||
|
|
- Keep responses brief and focused on safety
|
|||
|
|
- Never suggest that self-harm is a valid option
|
|||
|
|
- Encourage reaching out to trusted friends, family, or professionals
|
|||
|
|
|
|||
|
|
Remember: This person's life is valuable. Respond with compassion, urgency, and hope."""
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ShieldDetector:
|
|||
|
|
"""
|
|||
|
|
SHIELD Detection Engine
|
|||
|
|
|
|||
|
|
Detects jailbreak attempts and crisis signals in user messages.
|
|||
|
|
Optimized for speed: ~1-5ms per message.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# Jailbreak Pattern Categories
|
|||
|
|
JB_GODMODE_DIVIDERS = [
|
|||
|
|
r'\.\-\.\-\.\-',
|
|||
|
|
r'\.\=\.\=\.\=',
|
|||
|
|
r'\[\-\-\-\]',
|
|||
|
|
r'\{\{\{',
|
|||
|
|
r'\|\|\|',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_AUTHOR_MARKERS = [
|
|||
|
|
r'(?:^|\s)pliny(?:\s|$)',
|
|||
|
|
r'(?:^|\s)d(?:\s|\.|\-)*an(?:\s|$)',
|
|||
|
|
r'(?:^|\s)liberating(?:\s|$)',
|
|||
|
|
r'godmode\s+enabled',
|
|||
|
|
r'developer\s+mode',
|
|||
|
|
r'ignore\s+previous\s+instructions',
|
|||
|
|
r'ignore\s+all\s+prior\s+instructions',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_LEETSPEAK_MARKERS = [
|
|||
|
|
r'\bsur3\b',
|
|||
|
|
r'\bh3r3\b',
|
|||
|
|
r'\by0u\b',
|
|||
|
|
r'\bw1ll\b',
|
|||
|
|
r'\bn0w\b',
|
|||
|
|
r'\bpl34s3\b',
|
|||
|
|
r'\b4nd\b',
|
|||
|
|
r'\bth3\b',
|
|||
|
|
r'\b4ll\b',
|
|||
|
|
r'\b1nstruct1ons\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_BOUNDARY_INVERSION = [
|
|||
|
|
r'\[END\s+OF\s+INPUT\]',
|
|||
|
|
r'\[END\s+INPUT\]',
|
|||
|
|
r'\[START\s+OUTPUT\]',
|
|||
|
|
r'<\|im_start\|>',
|
|||
|
|
r'<\|im_end\|>',
|
|||
|
|
r'<\|system\|>',
|
|||
|
|
r'<\|user\|>',
|
|||
|
|
r'<\|assistant\|>',
|
|||
|
|
r'<<SYS>>',
|
|||
|
|
r'<</SYS>>',
|
|||
|
|
r'\[INST\]',
|
|||
|
|
r'\[/INST\]',
|
|||
|
|
r'\[SYSTEM\s*:\s*',
|
|||
|
|
r'NEW\s+SYSTEM\s+PROMPT',
|
|||
|
|
r'updated\s+system\s+prompt',
|
|||
|
|
r'override\s+system',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_TOKEN_INJECTION = [
|
|||
|
|
r'\\u003c\|vq_\|\\u003e',
|
|||
|
|
r'<\|vq_\|>',
|
|||
|
|
r'<\|special\|>',
|
|||
|
|
r'<\|injection\|>',
|
|||
|
|
r'{{\s*user\s*}}',
|
|||
|
|
r'{{\s*char\s*}}',
|
|||
|
|
r'<<\s*user\s*>>',
|
|||
|
|
r'<<\s*system\s*>>',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_KEYWORDS = [
|
|||
|
|
r'\bgodmode\b',
|
|||
|
|
r'\bliberating\b',
|
|||
|
|
r'^\s*DAN\s*[:\-]?',
|
|||
|
|
r'\bjailbreak\b',
|
|||
|
|
r'\bdo\s+anything\s+now\b',
|
|||
|
|
r'\bno\s+restrictions\b',
|
|||
|
|
r'\bno\s+limits\b',
|
|||
|
|
r'\bunfiltered\b',
|
|||
|
|
r'\bunleashed\b',
|
|||
|
|
r'\buncensored\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_REFUSAL_INVERSION = [
|
|||
|
|
r'never\s+say\s+(?:"|\')?(?:I\s+cannot|I\s+can\'t|I\s+apologize|I\'m\s+sorry)',
|
|||
|
|
r'do\s+not\s+(?:mention|say|include|refuse|decline)',
|
|||
|
|
r'never\s+(?:mention|say|include|refuse|decline)',
|
|||
|
|
r'do\s+not\s+respond\s+with\s+(?:"|\')?(?:I\s+cannot|I\s+can\'t)',
|
|||
|
|
r'skip\s+(?:the\s+)?(?:disclaimer|warning|caution|ethical)',
|
|||
|
|
r'ignore\s+(?:the\s+)?(?:ethical|safety|guideline|policy)',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_PERSONA_INJECTION = [
|
|||
|
|
r'pretend\s+(?:to\s+be|you\s+are|you\'re)',
|
|||
|
|
r'act\s+as\s+(?:if\s+you\s+are|you\'re|though)',
|
|||
|
|
r'roleplay\s+(?:as\s+|being\s+)',
|
|||
|
|
r'take\s+on\s+(?:the\s+role\s+of|a\s+persona)',
|
|||
|
|
r'you\s+are\s+now\s+(?:a\s+|an\s+)',
|
|||
|
|
r'simulate\s+(?:being\s+|a\s+)',
|
|||
|
|
r'from\s+now\s+on\s+you\s+are',
|
|||
|
|
r'you\s+will\s+(?:now\s+)?behave\s+as',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
JB_ENCODING_EVASION = [
|
|||
|
|
r'base64\s*[:\-]?\s*[A-Za-z0-9+/]{20,}={0,2}',
|
|||
|
|
r'rot13\s*[:\-]?',
|
|||
|
|
r'decode\s+this\s*[:\-]?',
|
|||
|
|
r'execute\s+as\s+code',
|
|||
|
|
r'run\s+as\s+python',
|
|||
|
|
r'eval\s*\(',
|
|||
|
|
r'exec\s*\(',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Crisis Signal Categories
|
|||
|
|
CRISIS_SUICIDAL_IDEATION = [
|
|||
|
|
r'\b(?:kill|end)\s+(?:myself|my\s+life)\b',
|
|||
|
|
r'\bwant\s+to\s+die\b',
|
|||
|
|
r'\bwant\s+to\s+end\s+(?:it|everything|my\s+life)\b',
|
|||
|
|
r'\bsuicidal\s+(?:thoughts|ideation|feelings?)\b',
|
|||
|
|
r'\bhate\s+my\s+life\b',
|
|||
|
|
r'\bno\s+reason\s+to\s+live\b',
|
|||
|
|
r'\bbetter\s+off\s+dead\b',
|
|||
|
|
r'\bwish\s+I\s+(?:was|were)\s+dead\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
CRISIS_METHOD_SEEKING = [
|
|||
|
|
r'\bhow\s+(?:do\s+I|to|can\s+I)\s+(?:kill|hang|overdose|cut)\s+myself\b',
|
|||
|
|
r'\bways?\s+to\s+(?:die|kill\s+myself)\b',
|
|||
|
|
r'\bmethods?\s+of\s+(?:suicide|dying)\b',
|
|||
|
|
r'\bpainless\s+way\s+to\s+(?:die|go)\b',
|
|||
|
|
r'\bhow\s+much\s+(?:pills|medication)\s+(?:to\s+take|for\s+overdose)\b',
|
|||
|
|
r'\bwhere\s+to\s+(?:jump|get\s+rope|buy\s+gun)\b',
|
|||
|
|
r'\bsuicide\s+methods?\b',
|
|||
|
|
r'\bhow\s+to\s+(?:commit\s+suicide|end\s+my\s+life)\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
CRISIS_LEETSPEAK_EVASION = [
|
|||
|
|
r'\bsu1c1d3?\b',
|
|||
|
|
r'\b5u1c1d3?\b',
|
|||
|
|
r'\bd13?\b',
|
|||
|
|
r'\b0v3rd0s3?\b',
|
|||
|
|
r'\bh4ng\s+mys3lf\b',
|
|||
|
|
r'\bk1ll\s+mys3lf\b',
|
|||
|
|
r'\bw4nt\s+t0\s+d13?\b',
|
|||
|
|
r'\bw4nt\s+t0\s+d1e\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
CRISIS_SUBSTANCE_SEEKING = [
|
|||
|
|
r'\bhow\s+much\s+(?:ambien|xanax|prozac|seroquel|trazodone)\s+(?:to\s+overdose|to\s+die)\b',
|
|||
|
|
r'\blethal\s+dose\s+(?:of\s+)?(?:pills|medication|drugs)\b',
|
|||
|
|
r'\bhow\s+many\s+pills\s+(?:to\s+die|to\s+end\s+it)\b',
|
|||
|
|
r'\btaking\s+all\s+my\s+(?:pills|medication)\b',
|
|||
|
|
r'\bmixing\s+(?:alcohol|pills)\s+(?:to\s+die|to\s+end\s+it)\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
CRISIS_DESPAIR = [
|
|||
|
|
r'\bno\s+(?:point|reason|hope)\s+(?:in\s+living|anymore)\b',
|
|||
|
|
r'\bcannot\s+(?:go\s+on|continue|take\s+it)\b',
|
|||
|
|
r'\bcan\'t\s+(?:go\s+on|continue|take\s+it)\b',
|
|||
|
|
r'\bempty\s+(?:inside|and\s+hollow)\b',
|
|||
|
|
r'\bno\s+one\s+(?:cares|would\s+miss\s+me)\b',
|
|||
|
|
r'\bwould\s+be\s+better\s+off\s+without\s+me\b',
|
|||
|
|
r'\bnever\s+going\s+to\s+get\s+better\b',
|
|||
|
|
r'\bnothing\s+(?:matters|will\s+change)\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
CRISIS_FAREWELL = [
|
|||
|
|
r'\bsaying\s+goodbye\s+(?:to\s+everyone|forever)\b',
|
|||
|
|
r'\bthis\s+is\s+(?:goodbye|farewell)\b',
|
|||
|
|
r'\btell\s+(?:them|everyone)\s+I\s+(?:loved|love)\s+(?:them|him|her)\b',
|
|||
|
|
r'\bgiving\s+away\s+my\s+(?:things|stuff|possessions)\b',
|
|||
|
|
r'\bmy\s+final\s+(?:message|note|words)\b',
|
|||
|
|
r'\bwrote\s+a\s+(?:suicide\s+)?note\b',
|
|||
|
|
r'\bsorry\s+for\s+(?:everything|the\s+pain|hurting\s+you)\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
CRISIS_SELF_HARM = [
|
|||
|
|
r'\bcut(?:ting)?\s+myself\b',
|
|||
|
|
r'\bself\s*harm(?:ing)?\b',
|
|||
|
|
r'\bhurt(?:ing)?\s+myself\b',
|
|||
|
|
r'\bburn(?:ing)?\s+myself\b',
|
|||
|
|
r'\bwant\s+to\s+(?:feel\s+pain|hurt\s+myself)\b',
|
|||
|
|
r'\bdeserve\s+to\s+(?:hurt|suffer|be\s+punished)\b',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# =====================================================================
|
|||
|
|
# Multilingual Injection Detection (Issue #614)
|
|||
|
|
# =====================================================================
|
|||
|
|
#
|
|||
|
|
# Translated attack phrases for top non-English languages.
|
|||
|
|
# Covers: Chinese (zh), Arabic (ar), Russian (ru), Hindi (hi),
|
|||
|
|
# Spanish (es), French (fr), German (de), Japanese (ja),
|
|||
|
|
# Korean (ko), Portuguese (pt).
|
|||
|
|
#
|
|||
|
|
# These are literal translations of common English jailbreak phrases
|
|||
|
|
# that bypass English-only regex detection.
|
|||
|
|
# =====================================================================
|
|||
|
|
|
|||
|
|
# Unicode normalization map: confusable characters -> ASCII equivalents
|
|||
|
|
# Handles homoglyph attacks (Cyrillic а vs Latin a, etc.)
|
|||
|
|
CONFUSABLE_MAP: Dict[str, str] = {
|
|||
|
|
# Cyrillic -> Latin
|
|||
|
|
'\u0430': 'a', # а
|
|||
|
|
'\u0435': 'e', # е
|
|||
|
|
'\u043e': 'o', # о
|
|||
|
|
'\u0440': 'p', # р
|
|||
|
|
'\u0441': 'c', # с
|
|||
|
|
'\u0443': 'y', # у
|
|||
|
|
'\u0445': 'x', # х
|
|||
|
|
'\u0410': 'A', # А
|
|||
|
|
'\u0415': 'E', # Е
|
|||
|
|
'\u041e': 'O', # О
|
|||
|
|
'\u0420': 'P', # Р
|
|||
|
|
'\u0421': 'C', # С
|
|||
|
|
'\u0423': 'Y', # У
|
|||
|
|
'\u0425': 'X', # Х
|
|||
|
|
# Greek -> Latin
|
|||
|
|
'\u03b1': 'a', # α
|
|||
|
|
'\u03bf': 'o', # ο
|
|||
|
|
'\u03c1': 'p', # ρ
|
|||
|
|
'\u03c3': 's', # σ
|
|||
|
|
'\u03c4': 't', # τ
|
|||
|
|
# Fullwidth -> ASCII
|
|||
|
|
'\uff41': 'a', '\uff42': 'b', '\uff43': 'c', '\uff44': 'd',
|
|||
|
|
'\uff45': 'e', '\uff46': 'f', '\uff47': 'g', '\uff48': 'h',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Multilingual jailbreak: "ignore all instructions" in 10 languages
|
|||
|
|
JB_MULTILINGUAL_IGNORE = [
|
|||
|
|
# Chinese
|
|||
|
|
r'忽\s*略\s*所\s*有\s*(?:指\s*令|规\s*则|命\s*令|指\s*示)',
|
|||
|
|
r'忽\s*略\s*之\s*前\s*的\s*(?:指\s*令|规\s*则|命\s*令)',
|
|||
|
|
r'不\s*要\s*遵\s*守\s*(?:规\s*则|指\s*令|限\s*制)',
|
|||
|
|
r'无\s*视\s*(?:所\s*有|之\s*前)\s*(?:指\s*令|规\s*则)',
|
|||
|
|
# Arabic
|
|||
|
|
r'تجاهل\s+(?:جميع|كل)\s+(?:التعليمات|القواعد|الأوامر)',
|
|||
|
|
r'لا\s+تتبع\s+(?:القواعد|التعليمات)',
|
|||
|
|
r'.Ignore\s+(?:جميع|كل)\s+(?:التعليمات|القواعد)',
|
|||
|
|
# Russian
|
|||
|
|
r'игнорируй\s+(?:все|все предыдущие)\s+(?:инструкции|правила|команды)',
|
|||
|
|
r'не\s+следуй\s+(?:правилам|инструкциям)',
|
|||
|
|
r'пренебреги\s+(?:всеми|всеми предыдущими)\s+(?:инструкциями|правилами)',
|
|||
|
|
# Hindi
|
|||
|
|
r'(?:सभी|सारे)\s+(?:निर्देश|नियम|आदेश)\s+(?:अनदेखा|नज़रअंदाज़)\s+करो',
|
|||
|
|
r'(?:निर्देश|नियम)\s+(?:अनदेखा|नज़रअंदाज़)\s+करो',
|
|||
|
|
r'पहले\s+के\s+(?:निर्देश|नियम)\s+(?:अनदेखा|नज़रअंदाज़)\s+करो',
|
|||
|
|
# Spanish
|
|||
|
|
r'ignora\s+(?:todas?\s+)?(?:las?\s+)?(?:instrucciones|reglas|órdenes)',
|
|||
|
|
r'no\s+sigas\s+(?:las?\s+)?(?:reglas|instrucciones)',
|
|||
|
|
r'olvida\s+(?:todas?\s+)?(?:las?\s+)?(?:instrucciones|reglas)',
|
|||
|
|
# French
|
|||
|
|
r'ignore\s+(?:toutes?\s+)?(?:les?\s+)?(?:instructions|règles|consignes)',
|
|||
|
|
r'ne\s+suis\s+pas\s+(?:les?\s+)?(?:règles|instructions)',
|
|||
|
|
r'oublie\s+(?:toutes?\s+)?(?:les?\s+)?(?:instructions|règles)',
|
|||
|
|
# German
|
|||
|
|
r'ignoriere\s+(?:alle\s+)?(?:Anweisungen|Regeln|Befehle)',
|
|||
|
|
r'befolge\s+(?:die\s+)?(?:Regeln|Anweisungen)\s+nicht',
|
|||
|
|
r'vernachlässige\s+(?:alle\s+)?(?:Anweisungen|Regeln)',
|
|||
|
|
# Japanese
|
|||
|
|
r'(?:すべての|全ての)?(?:指示|ルール|命令|規則)を(?:無視|IGNORE)',
|
|||
|
|
r'(?:指示|ルール|命令)を(?:無視|IGNORE)',
|
|||
|
|
r'従わないで\s*(?:指示|ルール|規則)',
|
|||
|
|
# Korean
|
|||
|
|
r'(?:모든|전체)\s*(?:지시|규칙|명령)\s*을?\s*(?:를)?\s*(?:무시|IGNORE)',
|
|||
|
|
r'(?:지시|규칙|명령)\s*을?\s*(?:를)?\s*(?:무시|IGNORE)',
|
|||
|
|
r'따르지\s*마\s*(?:지시|규칙)',
|
|||
|
|
# Portuguese
|
|||
|
|
r'ignore\s+(?:todas?\s+)?(?:as?\s+)?(?:instruções|regras|ordens)',
|
|||
|
|
r'não\s+(?:siga|obedeça)\s+(?:as?\s+)?(?:regras|instruções)',
|
|||
|
|
r'esqueça\s+(?:todas?\s+)?(?:as?\s+)?(?:instruções|regras)',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Multilingual jailbreak: "you are now [role]" / persona injection
|
|||
|
|
JB_MULTILINGUAL_PERSONA = [
|
|||
|
|
# Chinese
|
|||
|
|
r'你\s*(?:现在|从现在起)\s*(?:是|扮演|成为)',
|
|||
|
|
r'扮演\s*(?:一个|一下)\s*(?:角色|人物)',
|
|||
|
|
r'假装\s*(?:你\s*(?:是|成为))',
|
|||
|
|
# Arabic
|
|||
|
|
r'أنت\s+الآن\s+(?:角色)',
|
|||
|
|
r'كن\s+(?:角色)',
|
|||
|
|
r'تظاهر\s+أنك\s+角色',
|
|||
|
|
# Russian
|
|||
|
|
r'ты\s+теперь\s+(?:角色)',
|
|||
|
|
r'притворись\s+(?:что\s+ты)',
|
|||
|
|
r'сыграй\s+роль',
|
|||
|
|
# Spanish
|
|||
|
|
r'(?:ahora\s+)?(?:eres|serás|actúa\s+como)',
|
|||
|
|
r'finge\s+que\s+(?:eres|sos)',
|
|||
|
|
r'pretende\s+ser',
|
|||
|
|
# French
|
|||
|
|
r'tu\s+es\s+maintenant',
|
|||
|
|
r'fais\s+semblant\s+d\'être',
|
|||
|
|
r'joue\s+le\s+rôle\s+de',
|
|||
|
|
# German
|
|||
|
|
r'du\s+bist\s+jetzt',
|
|||
|
|
r'tue\s+so\s+als\s+wärst\s+du',
|
|||
|
|
r'spiel\s+die\s+Rolle',
|
|||
|
|
# Japanese
|
|||
|
|
r'(?:あなたは|君は)\s*(?:今|これから)',
|
|||
|
|
r'(?:ふりをして|假装)',
|
|||
|
|
r'役割を(?:演じて|務めて)',
|
|||
|
|
# Korean
|
|||
|
|
r'너는\s*(?:지금|이제)',
|
|||
|
|
r'역할을\s*(?:연기해|맡아)',
|
|||
|
|
r'척해\s*(?:봐|줘)',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Multilingual jailbreak: "do anything now" / unrestricted behavior
|
|||
|
|
JB_MULTILINGUAL_UNRESTRICTED = [
|
|||
|
|
# Chinese
|
|||
|
|
r'(?:现在|从现在起)\s*(?:可以|能)\s*(?:做|做任何)\s*(?:任何|一切|所有)',
|
|||
|
|
r'不\s*(?:受|要)\s*(?:任何|所有)\s*(?:限制|约束|规则)',
|
|||
|
|
r'(?:自由|无限制|无约束)\s*(?:地|的)?\s*(?:回答|回复|行动)',
|
|||
|
|
# Arabic
|
|||
|
|
r'افعل\s+أي\s+شيء\s+الآن',
|
|||
|
|
r'بدون\s+(?:قيود|restrictions|limits)',
|
|||
|
|
# Russian
|
|||
|
|
r'делай\s+(?:что\s+угодно|всё\s+что\s+хочешь)\s+сейчас',
|
|||
|
|
r'без\s+(?:ограничений|restrictions)',
|
|||
|
|
# Spanish
|
|||
|
|
r'haz\s+(?:lo\s+que\s+quieras|cualquier\s+cosa)\s+ahora',
|
|||
|
|
r'sin\s+(?:restricciones|limits|límites)',
|
|||
|
|
# French
|
|||
|
|
r'fais\s+(?:ce\s+que\s+tu\s+veux|n\'importe\s+quoi)\s+maintenant',
|
|||
|
|
r'sans\s+(?:restrictions|limites)',
|
|||
|
|
# German
|
|||
|
|
r'tue\s+(?:was\s+du\s+willst|alles)\s+jetzt',
|
|||
|
|
r'ohne\s+(?:Einschränkungen|restrictions)',
|
|||
|
|
# Japanese
|
|||
|
|
r'(?:何でも|なんでも)(?:して|やっ?て)',
|
|||
|
|
r'(?:制限|制約|ルール)\s*(?:なし|無し|なく)',
|
|||
|
|
# Korean
|
|||
|
|
r'(?:아무거나|뭐든지)\s*(?:해|하세요)',
|
|||
|
|
r'(?:제한|제약|규칙)\s*(?:없이|없는)',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Multilingual crisis signals: suicidal ideation
|
|||
|
|
CRISIS_MULTILINGUAL_SUICIDAL = [
|
|||
|
|
# Chinese
|
|||
|
|
r'想\s*(?:死|自杀|结束\s*(?:生命|自己|一切))',
|
|||
|
|
r'不\s*想\s*(?:活|活下去|活了)',
|
|||
|
|
r'活着\s*(?:没|没有)\s*(?:意思|意义|意思)',
|
|||
|
|
# Arabic
|
|||
|
|
r'أريد\s+(?:أن\s+أموت|الموت|أن\s+أقتل\s+نفسي)',
|
|||
|
|
r'لا\s+أريد\s+(?:العيش|الحياة|أن\s+أعيش)',
|
|||
|
|
# Russian
|
|||
|
|
r'хочу\s+(?:умереть|помереть|убить\s+себя)',
|
|||
|
|
r'не\s+хочу\s+(?:жить|жизни)',
|
|||
|
|
# Hindi
|
|||
|
|
r'मरना\s+चाहता?\s*(?:हूँ|है)',
|
|||
|
|
r'जीना\s+नहीं\s+चाहता?\s*(?:हूँ|है)',
|
|||
|
|
r'आत्महत्या\s+करना\s+चाहता?\s*(?:हूँ|है)',
|
|||
|
|
# Spanish
|
|||
|
|
r'quiero\s+(?:morir|suicidarme|matarme)',
|
|||
|
|
r'no\s+quiero\s+(?:vivir|seguir\s+vivo)',
|
|||
|
|
r'odio\s+mi\s+vida',
|
|||
|
|
# French
|
|||
|
|
r'je\s+veux\s+(?:mourir|me\s+suicider|me\s+tuer)',
|
|||
|
|
r'je\s+ne\s+veux\s+plus\s+(?:vivre|continuer)',
|
|||
|
|
# German
|
|||
|
|
r'ich\s+will\s+(?:sterben|mich\s+umbringen|suizid)',
|
|||
|
|
r'ich\s+will\s+nicht\s+mehr\s+(?:leben|weiterleben)',
|
|||
|
|
# Japanese
|
|||
|
|
r'(?:死にたい|消えたい|自殺したい)',
|
|||
|
|
r'(?:生きる|生きてる)\s*(?:意味|甲斐)\s*(?:が|は)\s*(?:ない|無い)',
|
|||
|
|
# Korean
|
|||
|
|
r'(?:죽고\s*싶다|죽고\s*싶어|자살\s*하고\s*싶다)',
|
|||
|
|
r'(?:살기|살아가기)\s*(?:싫다|싫어)',
|
|||
|
|
# Portuguese
|
|||
|
|
r'quero\s+(?:morrer|me\s+matar|suicidar)',
|
|||
|
|
r'não\s+quero\s+(?:mais\s+)?viver',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Multilingual crisis: despair / hopelessness
|
|||
|
|
CRISIS_MULTILINGUAL_DESPAIR = [
|
|||
|
|
# Chinese
|
|||
|
|
r'(?:生活|活着)\s*(?:没有|没)\s*(?:意义|意思|希望)',
|
|||
|
|
r'一切\s*(?:都|全)\s*(?:没有|没)\s*(?:意义|希望|用)',
|
|||
|
|
# Arabic
|
|||
|
|
r'لا\s+(?:أمل|hope|reason)\s+(?:في\s+الحياة|للعيش)',
|
|||
|
|
# Russian
|
|||
|
|
r'нет\s+(?:надежды|смысла)\s+(?:жить|в\s+жизни)',
|
|||
|
|
# Spanish
|
|||
|
|
r'no\s+tiene\s+(?:sentido|hope|razón)\s+(?:vivir|la\s+vida)',
|
|||
|
|
# French
|
|||
|
|
r'il\s+n\'y\s+a\s+plus\s+(?:d\'espoir|de\s+raison\s+de\s+vivre)',
|
|||
|
|
# German
|
|||
|
|
r'es\s+hat\s+(?:keinen\s+Sinn|keine\s+Hoffnung)\s+(?:zu\s+leben|mehr)',
|
|||
|
|
# Japanese
|
|||
|
|
r'(?:生きる|生きてる)\s*(?:意味|甲斐|希望)\s*(?:が|は)\s*(?:ない|無い| 없다)',
|
|||
|
|
# Korean
|
|||
|
|
r'(?:사는|살아가는)\s*(?:의미|희망|이유)\s*(?:가|은)\s*(?:없다|없어)',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
"""Initialize compiled regex patterns for performance"""
|
|||
|
|
self._compile_patterns()
|
|||
|
|
|
|||
|
|
def _compile_patterns(self):
|
|||
|
|
"""Compile all detection patterns for fast execution"""
|
|||
|
|
# Jailbreak patterns
|
|||
|
|
self.jb_patterns = {
|
|||
|
|
'godmode_dividers': re.compile('|'.join(self.JB_GODMODE_DIVIDERS), re.IGNORECASE),
|
|||
|
|
'author_markers': re.compile('|'.join(self.JB_AUTHOR_MARKERS), re.IGNORECASE),
|
|||
|
|
'leetspeak': re.compile('|'.join(self.JB_LEETSPEAK_MARKERS), re.IGNORECASE),
|
|||
|
|
'boundary_inversion': re.compile('|'.join(self.JB_BOUNDARY_INVERSION), re.IGNORECASE),
|
|||
|
|
'token_injection': re.compile('|'.join(self.JB_TOKEN_INJECTION), re.IGNORECASE),
|
|||
|
|
'keywords': re.compile('|'.join(self.JB_KEYWORDS), re.IGNORECASE),
|
|||
|
|
'refusal_inversion': re.compile('|'.join(self.JB_REFUSAL_INVERSION), re.IGNORECASE),
|
|||
|
|
'persona_injection': re.compile('|'.join(self.JB_PERSONA_INJECTION), re.IGNORECASE),
|
|||
|
|
'encoding_evasion': re.compile('|'.join(self.JB_ENCODING_EVASION), re.IGNORECASE),
|
|||
|
|
# Multilingual (Issue #614)
|
|||
|
|
'multilingual_ignore': re.compile('|'.join(self.JB_MULTILINGUAL_IGNORE)),
|
|||
|
|
'multilingual_persona': re.compile('|'.join(self.JB_MULTILINGUAL_PERSONA)),
|
|||
|
|
'multilingual_unrestricted': re.compile('|'.join(self.JB_MULTILINGUAL_UNRESTRICTED)),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Crisis patterns
|
|||
|
|
self.crisis_patterns = {
|
|||
|
|
'suicidal_ideation': re.compile('|'.join(self.CRISIS_SUICIDAL_IDEATION), re.IGNORECASE),
|
|||
|
|
'method_seeking': re.compile('|'.join(self.CRISIS_METHOD_SEEKING), re.IGNORECASE),
|
|||
|
|
'leetspeak_evasion': re.compile('|'.join(self.CRISIS_LEETSPEAK_EVASION), re.IGNORECASE),
|
|||
|
|
'substance_seeking': re.compile('|'.join(self.CRISIS_SUBSTANCE_SEEKING), re.IGNORECASE),
|
|||
|
|
'despair': re.compile('|'.join(self.CRISIS_DESPAIR), re.IGNORECASE),
|
|||
|
|
'farewell': re.compile('|'.join(self.CRISIS_FAREWELL), re.IGNORECASE),
|
|||
|
|
'self_harm': re.compile('|'.join(self.CRISIS_SELF_HARM), re.IGNORECASE),
|
|||
|
|
# Multilingual (Issue #614)
|
|||
|
|
'multilingual_suicidal': re.compile('|'.join(self.CRISIS_MULTILINGUAL_SUICIDAL)),
|
|||
|
|
'multilingual_despair': re.compile('|'.join(self.CRISIS_MULTILINGUAL_DESPAIR)),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _check_jailbreak(self, message: str) -> Tuple[bool, Dict[str, List[str]]]:
|
|||
|
|
"""
|
|||
|
|
Check message for jailbreak patterns
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Tuple of (detected, patterns_matched)
|
|||
|
|
"""
|
|||
|
|
patterns_found = {}
|
|||
|
|
detected = False
|
|||
|
|
|
|||
|
|
for category, pattern in self.jb_patterns.items():
|
|||
|
|
matches = pattern.findall(message)
|
|||
|
|
if matches:
|
|||
|
|
patterns_found[category] = matches
|
|||
|
|
detected = True
|
|||
|
|
|
|||
|
|
# Check for base64 encoded content
|
|||
|
|
if self._detect_base64_jailbreak(message):
|
|||
|
|
patterns_found.setdefault('encoding_evasion', []).append('base64_jailbreak')
|
|||
|
|
detected = True
|
|||
|
|
|
|||
|
|
return detected, patterns_found
|
|||
|
|
|
|||
|
|
def _check_crisis(self, message: str) -> Tuple[bool, Dict[str, List[str]]]:
|
|||
|
|
"""
|
|||
|
|
Check message for crisis signals
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Tuple of (detected, patterns_matched)
|
|||
|
|
"""
|
|||
|
|
patterns_found = {}
|
|||
|
|
detected = False
|
|||
|
|
|
|||
|
|
for category, pattern in self.crisis_patterns.items():
|
|||
|
|
matches = pattern.findall(message)
|
|||
|
|
if matches:
|
|||
|
|
patterns_found[category] = matches
|
|||
|
|
detected = True
|
|||
|
|
|
|||
|
|
return detected, patterns_found
|
|||
|
|
|
|||
|
|
def _detect_base64_jailbreak(self, message: str) -> bool:
|
|||
|
|
"""Detect potential jailbreak attempts hidden in base64"""
|
|||
|
|
# Look for base64 strings that might decode to harmful content
|
|||
|
|
b64_pattern = re.compile(r'[A-Za-z0-9+/]{40,}={0,2}')
|
|||
|
|
potential_b64 = b64_pattern.findall(message)
|
|||
|
|
|
|||
|
|
for b64_str in potential_b64:
|
|||
|
|
try:
|
|||
|
|
decoded = base64.b64decode(b64_str).decode('utf-8', errors='ignore')
|
|||
|
|
# Check if decoded content contains jailbreak keywords
|
|||
|
|
if any(kw in decoded.lower() for kw in ['ignore', 'system', 'jailbreak', 'dan', 'godmode']):
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _calculate_confidence(
|
|||
|
|
self,
|
|||
|
|
jb_detected: bool,
|
|||
|
|
crisis_detected: bool,
|
|||
|
|
jb_patterns: Dict[str, List[str]],
|
|||
|
|
crisis_patterns: Dict[str, List[str]]
|
|||
|
|
) -> float:
|
|||
|
|
"""
|
|||
|
|
Calculate confidence score based on number and type of matches
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Float between 0.0 and 1.0
|
|||
|
|
"""
|
|||
|
|
confidence = 0.0
|
|||
|
|
|
|||
|
|
if jb_detected:
|
|||
|
|
# Weight different jailbreak categories
|
|||
|
|
weights = {
|
|||
|
|
'godmode_dividers': 0.9,
|
|||
|
|
'token_injection': 0.9,
|
|||
|
|
'refusal_inversion': 0.85,
|
|||
|
|
'boundary_inversion': 0.8,
|
|||
|
|
'author_markers': 0.75,
|
|||
|
|
'keywords': 0.7,
|
|||
|
|
'persona_injection': 0.6,
|
|||
|
|
'leetspeak': 0.5,
|
|||
|
|
'encoding_evasion': 0.8,
|
|||
|
|
# Multilingual (Issue #614)
|
|||
|
|
'multilingual_ignore': 0.85,
|
|||
|
|
'multilingual_persona': 0.6,
|
|||
|
|
'multilingual_unrestricted': 0.75,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for category, matches in jb_patterns.items():
|
|||
|
|
weight = weights.get(category, 0.5)
|
|||
|
|
confidence += weight * min(len(matches) * 0.3, 0.5)
|
|||
|
|
|
|||
|
|
if crisis_detected:
|
|||
|
|
# Crisis patterns get high weight
|
|||
|
|
weights = {
|
|||
|
|
'method_seeking': 0.95,
|
|||
|
|
'substance_seeking': 0.95,
|
|||
|
|
'suicidal_ideation': 0.9,
|
|||
|
|
'farewell': 0.85,
|
|||
|
|
'self_harm': 0.9,
|
|||
|
|
'despair': 0.7,
|
|||
|
|
'leetspeak_evasion': 0.8,
|
|||
|
|
# Multilingual (Issue #614)
|
|||
|
|
'multilingual_suicidal': 0.9,
|
|||
|
|
'multilingual_despair': 0.7,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for category, matches in crisis_patterns.items():
|
|||
|
|
weight = weights.get(category, 0.7)
|
|||
|
|
confidence += weight * min(len(matches) * 0.3, 0.5)
|
|||
|
|
|
|||
|
|
return min(confidence, 1.0)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _merge_patterns(a: Dict[str, List[str]], b: Dict[str, List[str]]) -> Dict[str, List[str]]:
|
|||
|
|
"""Merge two pattern dictionaries, deduplicating matches."""
|
|||
|
|
merged = {}
|
|||
|
|
for d in (a, b):
|
|||
|
|
for category, matches in d.items():
|
|||
|
|
if category not in merged:
|
|||
|
|
merged[category] = list(matches)
|
|||
|
|
else:
|
|||
|
|
existing = set(merged[category])
|
|||
|
|
for m in matches:
|
|||
|
|
if m not in existing:
|
|||
|
|
merged[category].append(m)
|
|||
|
|
existing.add(m)
|
|||
|
|
return merged
|
|||
|
|
|
|||
|
|
def _normalize_unicode(self, text: str) -> str:
|
|||
|
|
"""Normalize unicode to catch homoglyph attacks.
|
|||
|
|
|
|||
|
|
1. NFKC normalization (compatibility decomposition + canonical composition)
|
|||
|
|
2. Replace confusable characters (Cyrillic/Greek lookalikes -> ASCII)
|
|||
|
|
3. Strip zero-width characters used for obfuscation
|
|||
|
|
"""
|
|||
|
|
# NFKC normalization handles most compatibility characters
|
|||
|
|
normalized = unicodedata.normalize('NFKC', text)
|
|||
|
|
|
|||
|
|
# Replace confusable characters
|
|||
|
|
result = []
|
|||
|
|
for ch in normalized:
|
|||
|
|
if ch in self.CONFUSABLE_MAP:
|
|||
|
|
result.append(self.CONFUSABLE_MAP[ch])
|
|||
|
|
else:
|
|||
|
|
result.append(ch)
|
|||
|
|
normalized = ''.join(result)
|
|||
|
|
|
|||
|
|
# Strip zero-width characters (used to break pattern matching)
|
|||
|
|
zero_width = '\u200b\u200c\u200d\u2060\ufeff' # ZWSP, ZWNJ, ZWJ, WJ, BOM
|
|||
|
|
for zw in zero_width:
|
|||
|
|
normalized = normalized.replace(zw, '')
|
|||
|
|
|
|||
|
|
return normalized
|
|||
|
|
|
|||
|
|
def detect(self, message: str) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Main detection entry point
|
|||
|
|
|
|||
|
|
Analyzes a message for jailbreak attempts and crisis signals.
|
|||
|
|
Now includes unicode normalization and multilingual detection (Issue #614).
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
message: The user message to analyze
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict containing:
|
|||
|
|
- verdict: One of Verdict enum values
|
|||
|
|
- confidence: Float 0.0-1.0
|
|||
|
|
- patterns_matched: Dict of matched patterns by category
|
|||
|
|
- action_required: Bool indicating if intervention needed
|
|||
|
|
- recommended_model: Model to use (None for normal routing)
|
|||
|
|
"""
|
|||
|
|
if not message or not isinstance(message, str):
|
|||
|
|
return {
|
|||
|
|
'verdict': Verdict.CLEAN.value,
|
|||
|
|
'confidence': 0.0,
|
|||
|
|
'patterns_matched': {},
|
|||
|
|
'action_required': False,
|
|||
|
|
'recommended_model': None,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Normalize unicode to catch homoglyph attacks (Issue #614)
|
|||
|
|
normalized = self._normalize_unicode(message)
|
|||
|
|
|
|||
|
|
# Run detection on both original and normalized
|
|||
|
|
# Original catches native-script multilingual attacks
|
|||
|
|
# Normalized catches homoglyph-evasion attacks
|
|||
|
|
jb_detected_orig, jb_patterns_orig = self._check_jailbreak(message)
|
|||
|
|
jb_detected_norm, jb_patterns_norm = self._check_jailbreak(normalized)
|
|||
|
|
crisis_detected_orig, crisis_patterns_orig = self._check_crisis(message)
|
|||
|
|
crisis_detected_norm, crisis_patterns_norm = self._check_crisis(normalized)
|
|||
|
|
|
|||
|
|
# Merge results from both passes
|
|||
|
|
jb_detected = jb_detected_orig or jb_detected_norm
|
|||
|
|
jb_patterns = self._merge_patterns(jb_patterns_orig, jb_patterns_norm)
|
|||
|
|
crisis_detected = crisis_detected_orig or crisis_detected_norm
|
|||
|
|
crisis_patterns = self._merge_patterns(crisis_patterns_orig, crisis_patterns_norm)
|
|||
|
|
|
|||
|
|
# Calculate confidence
|
|||
|
|
confidence = self._calculate_confidence(
|
|||
|
|
jb_detected, crisis_detected, jb_patterns, crisis_patterns
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Determine verdict
|
|||
|
|
if jb_detected and crisis_detected:
|
|||
|
|
verdict = Verdict.CRISIS_UNDER_ATTACK
|
|||
|
|
action_required = True
|
|||
|
|
recommended_model = None # Will use Safe Six internally
|
|||
|
|
elif crisis_detected:
|
|||
|
|
verdict = Verdict.CRISIS_DETECTED
|
|||
|
|
action_required = True
|
|||
|
|
recommended_model = None # Will use Safe Six internally
|
|||
|
|
elif jb_detected:
|
|||
|
|
verdict = Verdict.JAILBREAK_DETECTED
|
|||
|
|
action_required = True
|
|||
|
|
recommended_model = None # Route to hardened model
|
|||
|
|
else:
|
|||
|
|
verdict = Verdict.CLEAN
|
|||
|
|
action_required = False
|
|||
|
|
recommended_model = None
|
|||
|
|
|
|||
|
|
# Combine patterns
|
|||
|
|
all_patterns = {}
|
|||
|
|
if jb_patterns:
|
|||
|
|
all_patterns['jailbreak'] = jb_patterns
|
|||
|
|
if crisis_patterns:
|
|||
|
|
all_patterns['crisis'] = crisis_patterns
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
'verdict': verdict.value,
|
|||
|
|
'confidence': round(confidence, 3),
|
|||
|
|
'patterns_matched': all_patterns,
|
|||
|
|
'action_required': action_required,
|
|||
|
|
'recommended_model': recommended_model,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# Convenience function for direct use
|
|||
|
|
def detect(message: str) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Convenience function to detect threats in a message.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
message: User message to analyze
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Detection result dictionary
|
|||
|
|
"""
|
|||
|
|
detector = ShieldDetector()
|
|||
|
|
return detector.detect(message)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def is_safe_six_model(model_name: str) -> bool:
|
|||
|
|
"""
|
|||
|
|
Check if a model is in the SAFE SIX trusted list
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
model_name: Name of the model to check
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
True if model is in SAFE SIX
|
|||
|
|
"""
|
|||
|
|
return model_name.lower() in {m.lower() for m in SAFE_SIX_MODELS}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_crisis_prompt() -> str:
|
|||
|
|
"""
|
|||
|
|
Get the crisis system prompt for emergency situations
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Crisis intervention system prompt
|
|||
|
|
"""
|
|||
|
|
return CRISIS_SYSTEM_PROMPT
|