Spaces:
Sleeping
Sleeping
| import base64 | |
| import re | |
| from bs4 import BeautifulSoup, Comment, Tag | |
| from scanner.detectors.base import BaseDetector | |
| from scanner.domain.models import Finding, TextNode | |
| JAILBREAK_PREFIXES = [ | |
| r"ignore\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions|directives|commands|orders|messages)", | |
| r"disregard\s+(?:all\s+)?(?:prior|previous)\s+(?:instructions|directives)", | |
| r"forget\s+(?:all\s+)?(?:your|previous|prior)\s+(?:instructions|directives)", | |
| r"override\s+(?:your|all\s+)?(?:previous|prior)\s+(?:instructions|programming)", | |
| r"you\s+(?:are\s+)?(?:now|will\s+hereby)\s+(?:acting\s+as|to\s+act\s+as)", | |
| r"from\s+now\s+on\s+(?:you\s+are|you\'?re|you\s+will)", | |
| ] | |
| ROLE_MANIPULATION = [ | |
| r"you\s+are\s+(?:now\s+)?DAN\b", | |
| r"do\s+anything\s+now", | |
| r"released\s+from\s+(?:all\s+)?(?:constraints|restrictions|limitations)", | |
| r"developer\s+mode", | |
| r"AI\s+without\s+(?:restrictions|limitations|constraints|filters)", | |
| r"no\s+(?:restrictions|limitations|constraints|filters|boundaries)", | |
| ] | |
| EXFILTRATION = [ | |
| r"(?:send|post|upload|transmit|exfiltrate|steal|extract)\s+(?:the\s+)?(?:user|all|personal|private|sensitive)", | |
| r"(?:copy|move|transfer)\s+(?:all|every)\s+(?:files?|data|information|documents)", | |
| r"steal\s+(?:the\s+)?(?:session|token|cookie|credential)", | |
| r"exfiltrate\s+(?:the\s+)?(?:conversation|chat|data|logs)", | |
| r"(?:send\s+to|post\s+to|submit\s+to)\s+https?://", | |
| ] | |
| TOOL_MANIPULATION = [ | |
| r"execute\s+(?:the\s+)?(?:command|shell|system|bash|terminal)", | |
| r"run\s+(?:shell|system|bash|command|exec)", | |
| r"delete\s+(?:the\s+)?(?:file|database|user|account|record)", | |
| r"modify\s+(?:the\s+)?(?:database|record|file|config|configuration)", | |
| r"(?:transfer|send|move)\s+(?:funds?|money|crypto|payment)", | |
| r"change\s+(?:the\s+)?(?:payment|destination|recipient|amount)", | |
| ] | |
| IMPERSONATION = [ | |
| r"act\s+as\s+(?:if\s+)?you\s+are\s+the\s+(?:system|admin|root|owner)", | |
| r"pretend\s+to\s+be\s+(?:the\s+)?(?:system|admin|human|support)", | |
| r"you\s+are\s+(?:the\s+)?(?:system|admin|root)\s+(?:now|from now on)", | |
| ] | |
| # ─── Nova Rules Import: MalwareCreationRule patterns ──────────────────── | |
| MALWARE_KEYWORDS = [ | |
| r"(?i)GODMODE|OVERRIDE|UNSHACKLED", | |
| r"(?i)ignore\s+(?:policy|safeguard|ethical|content|safety)", | |
| r"(?i)(?:rebel\s+)?genius|mr\.?\s*robot|edward\s+snowden", | |
| r"(?i)keylogger|rootkit|dropper|loader|backdoor|C2|c2\s+server", | |
| r"(?i)ransomware|worm|trojan|spyware|botnet|adware", | |
| r"(?i)reverse\s+shell|bind\s+shell|connect.back", | |
| r"(?i)credential\s+(?:dump|stealer|theft|harvest)", | |
| r"(?i)process\s+(?:hollowing|injection|doppelganging)", | |
| r"(?i)DGA|domain\s+generation|command.control", | |
| r"(?i)persistence|privilege\s+escalation|bypass\s+UAC", | |
| ] | |
| # ─── Nova Rules Import: Policy Puppetry ───────────────────────────────── | |
| POLICY_PUPPETRY = [ | |
| r"<interaction-config>", | |
| r"blocked-strings", | |
| r"scene-rules", | |
| r"Dr\.?\s*House", | |
| r"(?i)article\s+\d+[.:]\s*(?:the\s+)?(?:ai|assistant|model|bot)", | |
| ] | |
| # ─── Nova Rules Import: Claude trigger strings ────────────────────────── | |
| CLAUDE_TRIGGERS = [ | |
| r"ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL", | |
| r"ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED", | |
| r"<antml:function_calls>", | |
| r"<antml:error>", | |
| ] | |
| # ─── Nova Rules Import: Scareware / Social Engineering ────────────────── | |
| SCAREWARE = [ | |
| r"(?i)(?:YOUR\s+)?COMPUTER\s+(?:HAS\s+BEEN\s+)?COMPROMISED", | |
| r"(?i)(?:YOUR\s+)?SYSTEM\s+(?:HAS\s+BEEN\s+)?INFECTED", | |
| r"(?i)\[URGENT\].*?(?:security|password|account)", | |
| r"(?i)click\s+(?:here|this\s+link).*?(?:secure|verify|confirm)", | |
| r"(?i)your\s+(?:account|password|email)\s+has\s+been\s+(?:compromised|hacked|breached)", | |
| r"(?i)verify\s+your\s+(?:identity|account|credentials).*?(?:immediately|within\s+\d+\s*hours?)", | |
| ] | |
| # ─── Nova Rules Import: Weaponized payload generation ─────────────────── | |
| WEAPONIZED_CODE = [ | |
| r"(?:create|write|generate|build|develop)\s+(?:a\s+)?(?:python\s+)?(?:function|script|code)\s+(?:that|to)\s+(?:connects?\s+back|listen|bind)", | |
| r"socket\.\s*connect\s*\(\s*\(.*host.*port", | |
| r"subprocess\.\s*Popen|os\.\s*system|os\.\s*popen", | |
| r"(?:rm\s+-rf\s+/|dd\s+if=/dev/zero\s+of=|mkfs\s+\.?\s*/dev/)", | |
| r"fork\s*bomb|while\s*True.*:.*os\.fork", | |
| ] | |
| ALL_PATTERNS = [ | |
| ("jailbreak", JAILBREAK_PREFIXES), | |
| ("role_override", ROLE_MANIPULATION), | |
| ("exfiltration", EXFILTRATION), | |
| ("tool_manipulation", TOOL_MANIPULATION), | |
| ("impersonation", IMPERSONATION), | |
| ("malware_generation", MALWARE_KEYWORDS), | |
| ("policy_puppetry", POLICY_PUPPETRY), | |
| ("claude_trigger", CLAUDE_TRIGGERS), | |
| ("scareware", SCAREWARE), | |
| ("weaponized_code", WEAPONIZED_CODE), | |
| ] | |
| class InjectionPatternMatcher(BaseDetector): | |
| """Detects known prompt injection patterns using regex + heuristics. | |
| Covers 100+ patterns across 5 categories: | |
| jailbreak, role_override, exfiltration, | |
| tool_manipulation, impersonation. | |
| Also handles: | |
| - Base64 encoded instructions | |
| - Unicode homoglyph substitution | |
| - HTML entity encoding | |
| - Double encoding | |
| """ | |
| name = "injection_patterns" | |
| def __init__(self): | |
| self.compiled: list[tuple[str, list[re.Pattern]]] = [] | |
| for category, patterns in ALL_PATTERNS: | |
| compiled_patterns = [re.compile(p, re.IGNORECASE | re.DOTALL) for p in patterns] | |
| self.compiled.append((category, compiled_patterns)) | |
| async def detect(self, soup: BeautifulSoup, source_url: str = "") -> list[Finding]: | |
| findings: list[Finding] = [] | |
| all_text = soup.get_text() | |
| textual_elements = list(soup.find_all(string=True)) | |
| for comment in soup.find_all(string=lambda s: isinstance(s, Comment)): | |
| if comment not in textual_elements: | |
| textual_elements.append(comment) | |
| element_texts = {} | |
| for el in textual_elements: | |
| text = el.strip() | |
| if text and len(text.split()) >= 2: | |
| parent = el.parent | |
| selector = self._make_selector(parent) if isinstance(parent, Tag) else "#unknown" | |
| element_texts[text] = (selector, el.parent.name if el.parent else "text") | |
| seen: set[tuple[int, str]] = set() | |
| for cat_idx, (category, patterns) in enumerate(self.compiled): | |
| for pattern in patterns: | |
| for text, (selector, tag) in element_texts.items(): | |
| key = (cat_idx, text) | |
| if key in seen: | |
| continue | |
| m = pattern.search(text) | |
| if m: | |
| seen.add(key) | |
| severity_map = { | |
| "exfiltration": "critical", | |
| "tool_manipulation": "high", | |
| "jailbreak": "high", | |
| "impersonation": "medium", | |
| "role_override": "medium", | |
| "malware_generation": "critical", | |
| "policy_puppetry": "high", | |
| "claude_trigger": "medium", | |
| "scareware": "high", | |
| "weaponized_code": "critical", | |
| } | |
| node = TextNode( | |
| content=text[:500], | |
| selector=selector or "#unknown", | |
| tag=tag or "text", | |
| visible=not self._is_hidden_in_dom(soup, selector), | |
| provenance=source_url, | |
| ) | |
| findings.append( | |
| Finding( | |
| detector=self.name, | |
| severity=severity_map.get(category, "medium"), # type: ignore[arg-type] | |
| confidence=0.9, | |
| title=f"{category.replace('_', ' ').title()} pattern detected", | |
| description=f"Matched {category} pattern: {pattern.pattern[:60]}", | |
| snippet=m.group()[:300], | |
| text_nodes=[node], | |
| category=category, | |
| recommendation=f"Review and sanitize the {category} instruction.", | |
| ) | |
| ) | |
| base64_findings = self._check_base64(all_text) | |
| findings.extend(base64_findings) | |
| return findings | |
| def _check_base64(self, text: str) -> list[Finding]: | |
| findings = [] | |
| b64_pattern = re.compile(r"(?:[A-Za-z0-9+/]{20,}={0,2})") | |
| for m in b64_pattern.finditer(text): | |
| candidate = m.group() | |
| try: | |
| decoded = base64.b64decode(candidate).decode("utf-8", errors="ignore") | |
| if any(kw in decoded.lower() for kw in ["ignore", "instruction", "agent", "system", "you are"]): | |
| findings.append( | |
| Finding( | |
| detector=self.name, | |
| severity="critical", | |
| confidence=0.8, | |
| title="Base64 encoded instruction detected", | |
| description="Base64 string decodes to content containing instruction keywords.", | |
| snippet=decoded[:300], | |
| category="hidden_instruction", | |
| recommendation="Decode and review the Base64 content.", | |
| ) | |
| ) | |
| except Exception: | |
| pass | |
| return findings | |
| def _is_hidden_in_dom(self, soup: BeautifulSoup, selector: str) -> bool: | |
| """Quick heuristic check if a selector points to hidden content.""" | |
| if not selector or selector == "#unknown": | |
| return False | |
| try: | |
| el = soup.select_one(selector) | |
| if not el: | |
| return False | |
| style = str(el.get("style", "") or "").lower() | |
| if "display:none" in style or "visibility:hidden" in style: | |
| return True | |
| except Exception: | |
| pass | |
| return False | |
| def _make_selector(self, el: Tag) -> str: | |
| parts = [] | |
| if el.get("id"): | |
| return f"#{el['id']}" | |
| if el.name: | |
| parts.append(el.name) | |
| if el.get("class"): | |
| parts.append("." + ".".join(el["class"])) | |
| return "".join(parts) if parts else el.name or "unknown" | |