From 93597520063ee018ff3b99c2aacacb4f79967039 Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:26:04 +0800 Subject: [PATCH] FIX: raise on characters AsciiSmugglerConverter cannot encode encode_message collected characters outside the ASCII printable range (0x20-0x7E), logged them at error level, and dropped them, while convert_async still returned a success ConverterResult. A non-ASCII objective was therefore smuggled as a corrupted or empty payload and the run reported success. Raise ValueError naming the offending characters and pointing to the lossless sibling converters instead. Adds regression tests (pre-fix: DID NOT RAISE) covering the reported non-ASCII cases, an astral-plane emoji, a control character, and the inclusive 0x20/0x7E boundaries. --- .../ascii_smuggler_converter.py | 27 ++++++++++++------- .../test_ascii_smuggler_converter.py | 21 +++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/pyrit/converter/token_smuggling/ascii_smuggler_converter.py b/pyrit/converter/token_smuggling/ascii_smuggler_converter.py index c91059d3ea..be18271662 100644 --- a/pyrit/converter/token_smuggling/ascii_smuggler_converter.py +++ b/pyrit/converter/token_smuggling/ascii_smuggler_converter.py @@ -59,26 +59,33 @@ def encode_message(self, *, message: str) -> tuple[str, str]: Returns: tuple[str, str]: A tuple with a summary of code points and the encoded message. + + Raises: + ValueError: If ``message`` contains a character outside the ASCII printable range (0x20-0x7E), + which has no Unicode Tag representation. Such characters were previously dropped while the + result still reported success; use ``SneakyBitsSmugglerConverter`` or + ``VariationSelectorSmugglerConverter`` to smuggle arbitrary Unicode losslessly. """ + invalid_chars = "".join(char for char in message if not 0x20 <= ord(char) <= 0x7E) + if invalid_chars: + raise ValueError( + f"Cannot encode {len(invalid_chars)} character(s) outside the ASCII printable range " + f"(0x20-0x7E): {invalid_chars!r}. Use SneakyBitsSmugglerConverter or " + f"VariationSelectorSmugglerConverter for lossless Unicode smuggling." + ) + encoded = "" code_points = "" - invalid_chars = "" if self.unicode_tags: encoded += chr(0xE0001) code_points += "U+E0001 " for char in message: - if 0x20 <= ord(char) <= 0x7E: - code_point = 0xE0000 + ord(char) - encoded += chr(code_point) - code_points += f"U+{code_point:X} " - else: - invalid_chars += char + code_point = 0xE0000 + ord(char) + encoded += chr(code_point) + code_points += f"U+{code_point:X} " if self.unicode_tags: encoded += chr(0xE007F) code_points += "U+E007F" - - if invalid_chars: - logger.error(f"Invalid characters detected: {invalid_chars}") return code_points, encoded def decode_message(self, *, message: str) -> str: diff --git a/tests/unit/converter/test_ascii_smuggler_converter.py b/tests/unit/converter/test_ascii_smuggler_converter.py index f5fe442862..30d48fcedb 100644 --- a/tests/unit/converter/test_ascii_smuggler_converter.py +++ b/tests/unit/converter/test_ascii_smuggler_converter.py @@ -46,3 +46,24 @@ async def test_ascii_smuggler_input_not_supported(): converter = AsciiSmugglerConverter(action="encode") with pytest.raises(ValueError, match="Input type not supported"): await converter.convert_async(prompt="test", input_type="image_path") + + +@pytest.mark.parametrize( + "prompt", ["caf\u00e9", "\u4f60\u597d\u4e16\u754c", "na\u00efve r\u00e9sum\u00e9", "\U0001f600", "line1\nline2"] +) +async def test_ascii_smuggler_encode_unrepresentable_chars_raise(prompt: str): + # Only the ASCII printable range (0x20-0x7E) maps to Unicode tags. Characters outside it used to be + # logged and dropped while convert_async still returned a success result, so a non-ASCII (or multi-line) + # objective was smuggled as a corrupted payload and an attack could be scored against a target that was + # never sent the objective. Encoding must fail loudly instead. + converter = AsciiSmugglerConverter(action="encode") + with pytest.raises(ValueError, match="ASCII printable range"): + await converter.convert_async(prompt=prompt, input_type="text") + + +async def test_ascii_smuggler_encode_printable_boundaries_do_not_raise(): + # 0x20 (space) and 0x7E (~) are the inclusive edges of the representable range and must still encode. + converter = AsciiSmugglerConverter(action="encode") + result = await converter.convert_async(prompt=" ~", input_type="text") + assert isinstance(result, ConverterResult) + assert result.output_type == "text"