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
1 change: 1 addition & 0 deletions docs/02_builtin.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ The following list provides an overview of each build-in plugin, further informa
| `homoglyph` | Encoding, Obfuscation | Substitutes Latin characters with visually-identical Unicode confusables (Cyrillic/Greek). Output looks unchanged to a human but uses different code points, evading ASCII keyword/regex filters. | `ratio` (fraction of mappable characters to substitute, 0.0-1.0, default: 1.0)<br> `seed` (optional integer seed for reproducible partial substitution) |
| `morse` | Encoding | Encodes text into Morse code. | N/A |
| `octal` | Encoding | Encodes text as space-separated ASCII/Unicode octal codepoint values. | `hint` (show the literal plaintext string `octal` in the encoded output, default true) |
| `zero_width` | Encoding, Obfuscation | Intersperses invisible zero-width Unicode characters between visible characters. Text looks unchanged to a human but contains no contiguous ASCII substring, evading keyword/regex filters. | `char` (which zero-width character to insert: `zwsp`/`zwnj`/`zwj`/`wj`/`zwnbsp`, default: `zwsp`)<br> `ratio` (fraction of gaps to fill, 0.0-1.0, default: 1.0)<br> `seed` (optional integer seed for reproducible partial output) |
| `best_of_n` | Obfuscation, Attack-Based | Implements ["Best-of-N Jailbreaking" John Hughes et al., 2024](https://arxiv.org/html/2412.03556v1#A1) to apply character scrambling, random capitalization, and character noising. | `variants` (number of variations to generate, default: 50) |
| `flip` | Obfuscation | Applies a flip attack to obfuscate text:<br> - FWO: Flip Word Order<br> - FCW: Flip Chars in Word<br> - FCS: Flip Chars in Sentence | `mode` (the flip mode to apply, default: `FWO`) |
| `mask` | Obfuscation, LLM | Masks high-risk words in the text with random character sequences, while providing a suffix that maps the masks back to the original words. | `advanced` (if true, creates multiple masks for longer words)<br> `advanced-split` (the number of characters per mask chunk for the advanced option, default: 6) |
Expand Down
136 changes: 136 additions & 0 deletions spikee/plugins/zero_width.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
Zero-Width Plugin

This plugin interleaves invisible zero-width Unicode characters between the
visible characters of the text. To a human reader (and in most UIs) the text
looks unchanged ("ignore previous instructions" still reads normally), but the
underlying byte sequence no longer contains any contiguous ASCII substring, which
defeats naive keyword/regex filters, substring denylists, and byte-level matching.

This is distinct from the existing plugins:
- `ascii_smuggler` *replaces* the text with invisible Unicode tag characters
(U+E00xx), so the visible content disappears entirely.
- `homoglyph` *substitutes* Latin glyphs with look-alike confusables from other
scripts, changing every visible code point.
Zero-width interspersing keeps the original visible glyphs intact and simply
injects invisible separators between them.

By default a separator is inserted in every gap (``ratio=1.0``), which is fully
deterministic. A lower ``ratio`` fills only that fraction of gaps, producing a
lighter obfuscation; pass ``seed`` for reproducible partial output. The invisible
character used is configurable via ``char``.

Usage:
spikee generate --plugins zero_width
spikee generate --plugins zero_width --plugin-options "zero_width:char=zwnj"
spikee generate --plugins zero_width --plugin-options "zero_width:ratio=0.5,seed=42"

Reference:
https://en.wikipedia.org/wiki/Zero-width_space
https://embracethered.com/blog/posts/2024/hiding-and-finding-text-with-unicode-tags/

Parameters:
text (str): The input text to be transformed.
char (str): Which zero-width character to insert. One of ``zwsp`` (U+200B,
default), ``zwnj`` (U+200C), ``zwj`` (U+200D), ``wj`` (U+2060), or
``zwnbsp``/``bom`` (U+FEFF).
ratio (float): Fraction of inter-character gaps to fill, in [0.0, 1.0]
(default: 1.0). At 1.0 a separator is inserted in every gap; below 1.0 a
random subset of gaps is chosen.
seed (int, optional): Seed for the random subset when ``ratio`` < 1.0, for
reproducible output.
exclude_patterns (List[str], optional): Supplied by the framework. Substrings
matching these regex patterns are preserved as-is.

Returns:
str: The transformed text with zero-width characters interspersed.
"""

import random
from typing import Dict

from spikee.templates.basic_plugin import BasicPlugin
from spikee.utilities.enums import ModuleTag
from spikee.utilities.hinting import ModuleDescriptionHint, ModuleOptionsHint
from spikee.utilities.modules import parse_options


class ZeroWidthPlugin(BasicPlugin):
DEFAULT_RATIO = 1.0
DEFAULT_CHAR = "zwsp"

# Supported invisible separators. Keys are the option values users pass.
ZERO_WIDTH_CHARS: Dict[str, str] = {
"zwsp": "​", # ZERO WIDTH SPACE
"zwnj": "‌", # ZERO WIDTH NON-JOINER
"zwj": "‍", # ZERO WIDTH JOINER
"wj": "⁠", # WORD JOINER
"zwnbsp": "", # ZERO WIDTH NO-BREAK SPACE (BOM)
"bom": "", # alias for zwnbsp
}

def get_description(self) -> ModuleDescriptionHint:
return (
[ModuleTag.ENCODING, ModuleTag.OBFUSCATION],
"Intersperses invisible zero-width characters between visible characters.",
)

def get_available_option_values(self) -> ModuleOptionsHint:
"""Return supported attack options; Tuple[options (default is first), llm_required]"""
return [
"char=zwsp",
"char=zwsp/zwnj/zwj/wj/zwnbsp (which zero-width character to insert)",
"ratio=N (0.0-1.0, fraction of inter-character gaps to fill)",
"seed=N (optional integer seed for reproducible partial output)",
], False

def plugin_transform(self, text: str, plugin_option: str = "") -> str:
"""
Intersperses zero-width characters between the characters of ``text``.

Args:
text (str): The input text (or chunk thereof).
plugin_option (str, optional): Plugin options string, e.g.
"char=zwnj,ratio=0.5,seed=42".

Returns:
str: The transformed text.
"""
zw_char, ratio, seed = self._parse_options(plugin_option)

if len(text) < 2 or ratio <= 0.0:
return text

if ratio >= 1.0:
return zw_char.join(text)

rng = random.Random(seed)
result = [text[0]]
for char in text[1:]:
if rng.random() < ratio:
result.append(zw_char)
result.append(char)
return "".join(result)

def _parse_options(self, plugin_option: str):
"""Parse the ``char``, ``ratio`` and ``seed`` options, falling back to defaults."""
opts = parse_options(plugin_option)

char_key = str(opts.get("char", self.DEFAULT_CHAR)).lower()
zw_char = self.ZERO_WIDTH_CHARS.get(char_key, self.ZERO_WIDTH_CHARS[self.DEFAULT_CHAR])

ratio = self.DEFAULT_RATIO
if "ratio" in opts:
try:
ratio = max(0.0, min(1.0, float(opts["ratio"])))
except ValueError:
ratio = self.DEFAULT_RATIO

seed = None
if "seed" in opts:
try:
seed = int(opts["seed"])
except ValueError:
seed = None

return zw_char, ratio, seed
2 changes: 1 addition & 1 deletion tests/functional/test_spikee_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_list_plugins(run_spikee, workspace_dir):

output_lines = spikee_list(run_spikee, workspace_dir, "plugins")
expected_local = {"test_repeat", "test_upper"}
expected_builtin = {"1337", "base64", "hex"}
expected_builtin = {"1337", "base64", "hex", "zero_width"}
_assert_contains(output_lines, expected_local | expected_builtin)


Expand Down