From 9ee00b1aa0527f5269dd5ebbd515870651b6953f Mon Sep 17 00:00:00 2001 From: yangnian Date: Wed, 8 Jul 2026 18:16:33 +0800 Subject: [PATCH] feat(analyzer): reduce false positive for emoji-zwj-sequence Signed-off-by: Yangnian --- src/skillspector/nodes/analyzers/common.py | 13 +++++++++++++ .../analyzers/static_patterns_prompt_injection.py | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/skillspector/nodes/analyzers/common.py b/src/skillspector/nodes/analyzers/common.py index 22bde49c..7e202039 100644 --- a/src/skillspector/nodes/analyzers/common.py +++ b/src/skillspector/nodes/analyzers/common.py @@ -20,6 +20,8 @@ import ast from typing import Any +import regex + from skillspector.models import Finding @@ -313,3 +315,14 @@ def get_source_segment(lines: list[str], lineno: int, end_lineno: int | None) -> start = max(0, lineno - 1) end = end_lineno or lineno return "\n".join(lines[start:end])[:200] + + +def is_emoji_zwj_sequence(content: str, zwj_idx: int): + + left = content[zwj_idx - 1] if zwj_idx > 0 else "" + right = content[zwj_idx + 1] if zwj_idx + 1 < len(content) else "" + + left_is_emoji = bool(regex.match(r"\p{Extended_Pictographic}", left)) + right_is_emoji = bool(regex.match(r"\p{Extended_Pictographic}", right)) + + return left_is_emoji and right_is_emoji diff --git a/src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py b/src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py index 43fda3e5..b3db6238 100644 --- a/src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py +++ b/src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py @@ -25,7 +25,7 @@ from skillspector.state import AnalyzerNodeResponse, SkillspectorState from . import static_runner -from .common import get_context, get_line_number +from .common import get_context, get_line_number, is_emoji_zwj_sequence from .pattern_defaults import PatternCategory logger = get_logger(__name__) @@ -155,6 +155,10 @@ def _first_smuggled_tag_offset(content: str) -> int | None: return i return None +def _check_emoji_zwj_sequence(content: str, match: re.Match[str]): + if 1 != len(match.group(0)) or '\u200d' != content[match.start():match.end()]: + return False + return is_emoji_zwj_sequence(content, match.start()) def analyze(content: str, file_path: str, file_type: str) -> list[AnalyzerFinding]: """Analyze content for prompt injection patterns (P1–P4).""" @@ -186,6 +190,8 @@ def ctx(start: int) -> str: if file_type in ("markdown", "other"): for pattern, confidence in P2_PATTERNS: for match in re.finditer(pattern, content, re.IGNORECASE | re.DOTALL): + if _check_emoji_zwj_sequence(content, match): + continue line_num = get_line_number(content, match.start()) findings.append( AnalyzerFinding(