Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1f86857
fix(p2): bound Markdown reference-comment findings
chrisknvidia Oct 14, 2026
9cdd1f8
test(p2): cover Markdown reference-comment boundaries
chrisknvidia Oct 14, 2026
4b7e0c9
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 15, 2026
7c9c5a3
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 15, 2026
629cae1
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 16, 2026
4072829
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 16, 2026
328c7db
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 16, 2026
dcf3618
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 16, 2026
67fcd5e
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 16, 2026
825ee25
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 16, 2026
83b1b3e
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 19, 2026
5f9f572
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 19, 2026
e95c591
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 19, 2026
9059d5d
Merge branch 'main' into feat/christopherk/p2-reference-comment-boundary
github-actions[bot] Oct 19, 2026
7e031d0
fix(p2): preserve shared keywords and reference destinations
chrisknvidia Oct 20, 2026
df2a71b
fix(p2): retain keyword-bearing reference destinations
chrisknvidia Oct 20, 2026
9f0b06d
fix(p2): reject incomplete reference destinations
chrisknvidia Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ def _is_p9_skipped_path(file_path: str) -> bool:
rf"(?:{_P2_IDENTIFIER_START}(?:system|POST|GET|send){_P2_IDENTIFIER_END}"
r"|ignore|instructions?|transmit)"
)
# Reference-comment titles need a source delimiter scan, rather than a DOTALL
# keyword expression that can backtrack past an already closed title.
_REFERENCE_COMMENT_GAP = r"[ \t]*(?:(?:\r\n|\r|\n)[ \t]*)?"
_REFERENCE_COMMENT_PATTERN = rf"\[//\]:{_REFERENCE_COMMENT_GAP}#"
_REFERENCE_COMMENT_OPENING = re.compile(_REFERENCE_COMMENT_PATTERN)
_REFERENCE_COMMENT_TITLE = re.compile(r"(?:[ \t]+|[ \t]*(?:\r\n|\r|\n)[ \t]*)\(")
_REFERENCE_COMMENT_SPAN = re.compile(r"[\s\S]*")
_REFERENCE_COMMENT_KEYWORD = re.compile(_P2_COMMENT_KEYWORD, re.IGNORECASE)
P2_PATTERNS = [
(
# "<!-->" and "<!--->" are abrupt empty comments: the HTML tokenizer
Expand All @@ -110,7 +118,7 @@ def _is_p9_skipped_path(file_path: str) -> bool:
rf"<!--(?!-?>)(?:(?!--!?>).)*?{_P2_COMMENT_KEYWORD}(?:(?!--!?>).)*?--!?>",
0.7,
),
(r"\[//\]:\s*#\s*\(.*?(?:system|instructions?|ignore|POST|GET|send|transmit).*?\)", 0.8),
(_REFERENCE_COMMENT_PATTERN, 0.8),
(_ZERO_WIDTH_PATTERN, 0.6),
(r"data:text/plain;base64,[A-Za-z0-9+/=]{50,}", 0.7),
]
Expand Down Expand Up @@ -281,6 +289,9 @@ def _p2_pattern_matches(
"""Yield all structured matches or the first control signal on each line."""
if check_runtime is not None:
check_runtime()
if pattern == _REFERENCE_COMMENT_PATTERN:
yield from _reference_comment_matches(content, check_runtime)
return
compiled = re.compile(pattern, re.IGNORECASE | re.DOTALL)
if pattern not in _SINGLE_CHARACTER_P2_PATTERNS:
for match in compiled.finditer(content):
Expand Down Expand Up @@ -310,6 +321,94 @@ def _p2_pattern_matches(
cursor = line_break.end()


def _reference_comment_matches(
content: str,
check_runtime: Callable[[], None] | None = None,
) -> Iterator[re.Match[str]]:
"""Own the destination and its separated, parenthesized title.

CommonMark destinations can contain balanced parentheses, while titles
require intervening whitespace. Titles allow escapes and line breaks but
no blank lines. A keyword already inside an unfinished or malformed title
remains conservative evidence; later visible paragraphs cannot supply it.
"""
cursor = 0
while cursor < len(content):
if check_runtime is not None:
check_runtime()
opening = _REFERENCE_COMMENT_OPENING.search(content, cursor)
if opening is None:
return
destination_start = opening.end() - 1
end = opening.end()
depth = 0
next_runtime_check = end
while end < len(content):
if check_runtime is not None and end >= next_runtime_check:
check_runtime()
next_runtime_check = end + 1024
char = content[end]
if char in " \t\r\n" or ord(char) < 32 or ord(char) == 127:
break
if char == "\\" and end + 1 < len(content) and content[end + 1] in "\\()":
end += 2
continue
if char == "(":
depth += 1
elif char == ")":
if depth == 0:
break
depth -= 1
end += 1
destination_valid = depth == 0 and (end == len(content) or content[end] in " \t\r\n")
title = _REFERENCE_COMMENT_TITLE.match(content, end) if destination_valid else None
if title is None:
if destination_valid and _REFERENCE_COMMENT_KEYWORD.search(
content, destination_start, end
):
match = _REFERENCE_COMMENT_SPAN.match(content, opening.start(), end)
assert match is not None
yield match
cursor = max(end, opening.end())
continue
title_start = title.end()
end = title_start
closed = False
line_has_text = True
next_runtime_check = title_start
while end < len(content):
if check_runtime is not None and end >= next_runtime_check:
check_runtime()
next_runtime_check = end + 1024
char = content[end]
if char == "\\" and end + 1 < len(content) and content[end + 1] in "\\()":
end += 2
line_has_text = True
continue
if char == ")":
end += 1
closed = True
break
if char in "\r\n":
if not line_has_text:
break
line_has_text = False
if char == "\r" and end + 1 < len(content) and content[end + 1] == "\n":
end += 1
elif char not in " \t":
line_has_text = True
end += 1
if check_runtime is not None:
check_runtime()
if _REFERENCE_COMMENT_KEYWORD.search(
content, destination_start, end - 1 if closed else end
):
match = _REFERENCE_COMMENT_SPAN.match(content, opening.start(), end)
assert match is not None
yield match
cursor = max(end, title_start)


def _first_smuggled_tag_offset(
content: str,
check_runtime: Callable[[], None] | None = None,
Expand Down
Loading
Loading