FEAT: add AcrosticConverter - #2280
Conversation
|
@microsoft-github-policy-service agree company="Microsoft" |
|
thanks for contributing Ivanodib! Curious: do you have a link to the paper where this method is documented? or is this something you tried in your own red teaming? |
Thanks Justin Song (@jsong468)! No specific paper, and I couldn't find a source for the input-side use specifically. I added it as the encoder counterpart of the existing FirstLetterConverter. I tested it with benign canaries on a few recent models (Kimi-K2.6, DeepSeek-V4-Pro, Grok-4.3, GPT-5.1, GPT-5.4) and they reconstruct and follow the hidden instruction. |
Justin Song (jsong468)
left a comment
There was a problem hiding this comment.
Awesome, thank you for taking the time to do this!
Description
Adds
AcrosticConverter, a text-to-text converter that hides the prompt as an acrostic. Each character of the prompt becomes its own line — an alphabetic character is expanded into a benign word starting with that character, and a space is rendered as a sentinel line — so the original message is legible only when reading the first character of each line from top to bottom. A leading instruction tells the model to reconstruct the hidden message and follow it.For example,
await AcrosticConverter().convert_async(prompt="cut a tree")produces:Reading the first character of each line gives
CUT A TREE.This is the encoder counterpart of the existing
FirstLetterConverter, which reduces text down to its first letters. The intended use is evaluating whether a content filter or a model detects a request hidden in acrostic form: a filter scanning the visible text sees an innocuous word list, while the real request only exists vertically.Both the leading instruction and the word bank are constructor parameters, so the surface text can be varied by the caller.
Scope and limitations. This is a text transformation primitive for red-team evaluation, not a validated bypass — effectiveness against any given filter or model should be measured empirically rather than assumed. The default word bank is deliberately minimal (one word per letter), which makes the output repetitive and fairly recognizable; callers wanting less conspicuous output should pass a richer bank via
word_bank.decode()is a round-trip helper tailored to this converter's own output format, not a general-purpose acrostic parser.I did not open an issue before submitting — happy to do that first if maintainers prefer.
Tests and Documentation
Tests — new file
tests/unit/converter/test_acrostic_converter.pywith 7 unit tests covering: the returnedConverterResultshape, first letters spelling the message, space encoding via the sentinel line, the encode → decode round trip, dropping of non-alphabetic characters, a custom instruction, and a custom word bank.Documentation — registered in
pyrit/converter/__init__.py(import +__all__) and added todoc/code/converters/1_text_to_text_converters.pyunder §1.2 Obfuscation Converters, next toFirstLetterConverter. This satisfiestests/unit/docs/test_converter_documentation.py, which requires every converter to appear in a converter notebook.JupyText — I updated the
.pypercent file but did not re-execute the notebook to regenerate1_text_to_text_converters.ipynb. That notebook's LLM-Based Converters section connects to live endpoints I don't have configured, and re-executing it would rewrite the outputs of every cell and bloat the diff well beyond this change. Happy to regenerate it if a maintainer prefers, otherwise it should be covered by the pre-release regeneration pass described indoc/contributing/7_notebooks.md.Checks run locally
uv run pytest tests/unit/converter tests/unit/docs tests/unit/registry→ 1488 passeduv run pre-commit run --files <changed files>→ all hooks pass, includingruff-format,ruff-check,ty,check-no-rest-rolesandcheck-async-suffix