Skip to content

BUG: AsciiSmugglerConverter silently drops non-ASCII characters #2539

Description

@chiruu12

Describe the bug

AsciiSmugglerConverter silently removes every character outside the ASCII printable range 0x20-0x7E. The dropped characters go to logger.error and nowhere else, so convert_async returns a ConverterResult that reports success while carrying a payload that is not the prompt.

café encodes to a payload that decodes back to caf. 你好世界 encodes to an empty payload, or to nothing but the two control tags when unicode_tags=True. An attack run against a non-English objective then scores as "attack did not succeed" against a target that was never sent the objective.

The two sibling converters in the same package do not have this problem. SneakyBitsSmugglerConverter and VariationSelectorSmugglerConverter both encode the UTF-8 bytes and round-trip Unicode losslessly.

There is a second, smaller problem in decode_message. The report of whether anything was hidden compares the decoded length to the input length:

if len(decoded_message) != len(message):
    logger.info("Hidden Unicode Tags discovered.")
else:
    logger.info("No hidden Unicode Tag characters discovered.")

Those two lengths are equal exactly when every character was a hidden tag, so decoding this converter's own unicode_tags=False output logs No hidden Unicode Tag characters discovered. for a string that is entirely hidden tags.

Steps/Code to Reproduce

import asyncio

from pyrit.converter import AsciiSmugglerConverter, SneakyBitsSmugglerConverter


async def main():
    for cls in (AsciiSmugglerConverter, SneakyBitsSmugglerConverter):
        for prompt in ["café", "你好世界"]:
            encoded = await cls(action="encode").convert_async(prompt=prompt, input_type="text")
            decoded = await cls(action="decode").convert_async(prompt=encoded.output_text, input_type="text")
            print(cls.__name__, repr(prompt), "->", repr(decoded.output_text))

    # Second problem: decoding a fully hidden message reports that nothing was hidden.
    encoded = await AsciiSmugglerConverter(action="encode", unicode_tags=False).convert_async(
        prompt="Hello", input_type="text"
    )
    await AsciiSmugglerConverter(action="decode").convert_async(prompt=encoded.output_text, input_type="text")


asyncio.run(main())

Expected Results

Round-trip through AsciiSmugglerConverter returns the prompt it was given, as it does for the other two smuggling converters.

If Unicode Tags cannot represent a character, the converter should say so through the result rather than dropping the character and returning success.

decode_message should report that hidden tags were found when they were found.

Actual Results

AsciiSmugglerConverter 'café' -> 'caf'
AsciiSmugglerConverter '你好世界' -> ''
SneakyBitsSmugglerConverter 'café' -> 'café'
SneakyBitsSmugglerConverter '你好世界' -> '你好世界'
INFO:pyrit.converter.token_smuggling.ascii_smuggler_converter:No hidden Unicode Tag characters discovered.

Additional context

This is the same class of defect as #2308 (BrailleConverter) and #2383 (AcrosticConverter), both of which were fixed by preserving the input rather than dropping it. The Unicode Tags block only has code points for 0x20-0x7E, so pass-through is not available here in the same form, and the fix needs a decision on which of these you want:

  1. Encode the UTF-8 bytes, as SneakyBitsSmugglerConverter already does. Lossless, but changes the encoding for existing ASCII payloads unless it is opt-in.
  2. Leave unrepresentable characters in the output as plain text. Lossless and matches BUG: BrailleConverter silently drops characters outside its lookup table #2308, but the plaintext is then visible.
  3. Raise on unrepresentable input. Lossless in the sense that nothing is silently wrong, and it is the smallest change.

Happy to open the PR for whichever you prefer. Existing coverage in tests/unit/converter/test_ascii_smuggler_converter.py only passes hi and hello, so there is no non-ASCII case in the suite today.

Duplicate check performed on 2026-09-02: searched open and closed issues and pull requests for AsciiSmuggler, smuggler, unicode tags, dropped characters, silently drops, and non-ASCII, and read the titles of all 50 open pull requests. #479, #827 and #842 built the converter and #1967 is a closed refactor. Nothing covers this.

Versions

  • OS: macOS 15.5
  • Python version: 3.12.11
  • PyRIT version: installed from main in editable mode at 6d5b1a9

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions