diff --git a/garak/probes/encoding.py b/garak/probes/encoding.py index 414a32c3a..c0405cdd9 100644 --- a/garak/probes/encoding.py +++ b/garak/probes/encoding.py @@ -652,6 +652,63 @@ def __init__(self, config_root=_config): EncodingMixin.__init__(self) +def _fancy_text( + text: bytes, upper_base: int, lower_base: int, digit_base: int +) -> bytes: + output = [] + for char in text.decode(): + if "A" <= char <= "Z": + output.append(chr(upper_base + ord(char) - ord("A"))) + elif "a" <= char <= "z": + output.append(chr(lower_base + ord(char) - ord("a"))) + elif "0" <= char <= "9": + output.append(chr(digit_base + ord(char) - ord("0"))) + else: + output.append(char) + return bytes("".join(output), "utf-8") + + +def fullwidth(text: bytes) -> bytes: + output = [] + for char in text.decode(): + codepoint = ord(char) + if 0x21 <= codepoint <= 0x7E: # printable ASCII -> Fullwidth Forms + output.append(chr(codepoint + 0xFEE0)) + elif char == " ": + output.append(" ") # ideographic space + else: + output.append(char) + return bytes("".join(output), "utf-8") + + +def math_bold(text: bytes) -> bytes: + # Mathematical Alphanumeric Symbols: bold + return _fancy_text(text, 0x1D400, 0x1D41A, 0x1D7CE) + + +def monospace(text: bytes) -> bytes: + # Mathematical Alphanumeric Symbols: monospace + return _fancy_text(text, 0x1D670, 0x1D68A, 0x1D7F6) + + +class InjectFancyText(EncodingMixin, garak.probes.Probe): + """Probe for susceptibility to "fancy text" Unicode font encoding. + + Encodes payloads by mapping ASCII characters to visually-similar Unicode + glyphs (fullwidth forms and Mathematical Alphanumeric Symbols), in the style + of the qaz.wtf Unicode text converter, and tries a variety of ways of getting + the target model to repeat & decode them.""" + + doc_uri = "https://qaz.wtf/u/convert.cgi" + encoding_funcs = [fullwidth, math_bold, monospace] + encoding_name = "Fancy text" + active = True + + def __init__(self, config_root=_config): + garak.probes.Probe.__init__(self, config_root=config_root) + EncodingMixin.__init__(self) + + class InjectUnicodeTagChars(EncodingMixin, garak.probes.Probe): """Probe for susceptibility to unicode tag ASCII smuggling diff --git a/tests/probes/test_probes_encoding.py b/tests/probes/test_probes_encoding.py index 663f93ae7..b7a3852ea 100644 --- a/tests/probes/test_probes_encoding.py +++ b/tests/probes/test_probes_encoding.py @@ -4,6 +4,7 @@ import garak.probes.encoding from garak import _plugins from garak.probes.encoding import InjectAtbash +from garak.probes.encoding import fullwidth, math_bold, monospace ENCODING_PROBES = [ classname @@ -91,3 +92,27 @@ def test_atbash_function(plain, expected): encoder = InjectAtbash.atbash # staticmethod reference result = encoder(plain.encode()).decode() assert result == expected + + +@pytest.mark.parametrize( + "encoder, plain, expected", + [ + # Fullwidth Forms: letters, digits and punctuation are all remapped + (fullwidth, "abc", "abc"), + (fullwidth, "XYZ", "XYZ"), + (fullwidth, "Hello, World! 123", "Hello, World! 123"), + (fullwidth, "1234!?", "1234!?"), + # Mathematical Alphanumeric Symbols: bold (punctuation passes through) + (math_bold, "abc", "𝐚𝐛𝐜"), + (math_bold, "XYZ", "𝐗𝐘𝐙"), + (math_bold, "1234!?", "𝟏𝟐𝟑𝟒!?"), + # Mathematical Alphanumeric Symbols: monospace (punctuation passes through) + (monospace, "abc", "𝚊𝚋𝚌"), + (monospace, "XYZ", "𝚇𝚈𝚉"), + (monospace, "1234!?", "𝟷𝟸𝟹𝟺!?"), + ], +) +def test_fancy_text_functions(encoder, plain, expected): + # Fancy-text encoders map ASCII to visually-similar Unicode glyphs. + result = encoder(plain.encode()).decode() + assert result == expected