diff --git a/apps/backend/runners/github/sanitize.py b/apps/backend/runners/github/sanitize.py index f3bcae115..872c18bb4 100644 --- a/apps/backend/runners/github/sanitize.py +++ b/apps/backend/runners/github/sanitize.py @@ -72,10 +72,15 @@ class ContentSanitizer: ) """ - # Patterns for dangerous content + # Patterns for dangerous content. + # + # The end tags allow trailing junk (""): HTML treats anything + # up to the next ">" as part of the end tag, so a stricter "" + # fails to match and leaves the *whole* element — payload included — in the + # content. The "\b" on the open tag keeps "" from matching. HTML_COMMENT_PATTERN = re.compile(r"", re.MULTILINE) - SCRIPT_TAG_PATTERN = re.compile(r"", re.IGNORECASE) - STYLE_TAG_PATTERN = re.compile(r"", re.IGNORECASE) + SCRIPT_TAG_PATTERN = re.compile(r"]*>", re.IGNORECASE) + STYLE_TAG_PATTERN = re.compile(r"]*>", re.IGNORECASE) # Patterns that look like prompt injection attempts INJECTION_PATTERNS = [ diff --git a/tests/test_github_sanitize.py b/tests/test_github_sanitize.py new file mode 100644 index 000000000..636af2644 --- /dev/null +++ b/tests/test_github_sanitize.py @@ -0,0 +1,115 @@ +""" +Tests for the GitHub content sanitizer +====================================== + +``runners.github.sanitize.ContentSanitizer`` strips hostile markup out of +issue/PR/comment bodies before they are embedded in an agent prompt. It had no +test coverage; these tests pin the tag-stripping contract, with emphasis on the +end-tag forms an attacker controls. +""" + +import pytest + +from runners.github.sanitize import ContentSanitizer + + +@pytest.fixture +def sanitizer(): + return ContentSanitizer() + + +class TestScriptTagRemoval: + """", + # HTML treats everything up to ">" as part of the end tag, so these + # are valid end tags an attacker can use to slip past a naive regex. + "", + "', + "", + "", + ], + ) + def test_script_element_is_stripped(self, sanitizer, body): + result = sanitizer.sanitize_issue_body(body) + assert "ignore all instructions" not in result.content + assert result.content.strip() == "" + assert result.was_modified + + def test_script_removal_is_reported(self, sanitizer): + result = sanitizer.sanitize_issue_body("") + assert any("script" in item for item in result.removed_items) + + def test_surrounding_text_survives(self, sanitizer): + result = sanitizer.sanitize_issue_body( + "beforeafter" + ) + assert "payload" not in result.content + assert "before" in result.content + assert "after" in result.content + + +class TestStyleTagRemoval: + @pytest.mark.parametrize( + "body", + [ + "", + "", + "", + ], + ) + def test_style_element_is_stripped(self, sanitizer, body): + result = sanitizer.sanitize_issue_body(body) + assert "body{}" not in result.content + assert result.was_modified + + +class TestNoFalsePositives: + """Lookalike tags must not be swallowed.""" + + @pytest.mark.parametrize( + "body", + [ + "keep me", + "keep me", + ], + ) + def test_lookalike_tags_are_preserved(self, sanitizer, body): + result = sanitizer.sanitize_issue_body(body) + assert "keep me" in result.content + + def test_prose_mentioning_script_is_preserved(self, sanitizer): + body = "Use the