Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 17 additions & 10 deletions pyrit/converter/token_smuggling/ascii_smuggler_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/converter/test_ascii_smuggler_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"