Skip to content

Repository files navigation

turkish-casefold-evasion

turkish-casefold-evasion

"İGNORE".lower() is not "ignore". Your keyword filter just let the injection through.

Most hand-rolled prompt-injection filters do the same thing: text.lower() and then check for trigger substrings like "ignore all previous instructions". That is silently broken for Turkish. Python's locale-independent str.lower() maps the Turkish dotted capital İ (U+0130) to i + a combining dot above (U+0307) — not plain i:

>>> "İGNORE ALL PREVIOUS INSTRUCTIONS".lower()
'i̇gnore all previous instructions'          # note the extra dot on the first i
>>> "ignore all previous instructions" in "İGNORE ALL PREVIOUS INSTRUCTIONS".lower()
False                                          # the filter misses it

The dotless ı (U+0131) and Unicode confusables (Cyrillic/Greek/fullwidth lookalikes) do the same. To a multilingual LLM the text still reads as the instruction; to your filter it's invisible.

This repo measures the bypass, ships the evasion dataset, and gives the one-line fix.

License tests deps 🤗 dataset


📉 The finding

Applying three Turkish-flavoured evasions to 15 common trigger phrases, against a naive str.lower() + substring filter (not an ML guard):

Evasion Bypass rate vs naive filter ↑(attacker) Caught after the fix*
dotless-i (i→ı, I→İ) 91% 100%
confusable (Cyrillic/Greek lookalikes) 100% 100%
combining-mark (i + U+0307) 91% 100%
overall 94.6% 100%

94.6% of case-folded/confusable evasions slipped past the naive filter. Reproduce with python build_study.py (writes study.json + the dataset).

*The 100% "caught after the fix" is on this in-map evasion setnormalize() reverses exactly the transforms these generators apply, so it's a soundness check, not proof of universal coverage. normalize() folds the confusables in its map + NFKC; a homoglyph outside the map (e.g. U+0261 script-g, "iɡnore") still slips through. Extend the map for your threat model. The robust, un-gameable number here is the 94.6% bypass of the naive filter.

Note: this targets naive keyword/regex filters. Modern ML guards (e.g. ProtectAI's deberta) are far more robust to these tricks — see the companion study guard-blindspots-tr. If you rely on a keyword/regex layer at all, though, this bypass applies to you.

🩹 The fix (one function)

Normalize before matching — NFKC, fold confusables, strip combining marks, then casefold():

from tcfeval.core import normalize

def hardened_filter(text, triggers):
    norm = normalize(text)
    return any(normalize(t) in norm for t in triggers)

normalize("İGNORE")   # -> 'ignore'
normalize("ıgnore")   # -> 'ignore'

tcfeval.core.normalize() is dependency-free and safe to drop in front of any existing keyword/regex prompt-injection check.

📦 Dataset

data/evasion_dataset.jsonl — every (trigger, evasion) pair with whether it bypassed the naive filter and was caught by the hardened one. Also on the Hub:

from datasets import load_dataset
ds = load_dataset("fevziegeyurtsevenler/turkish-casefold-evasion")

Fields: trigger, evasion, evaded_text, original_caught_by_naive, evaded_caught_by_naive, evaded_caught_by_hardened, bypassed_naive_filter.

⚖️ Honesty & limits

  • The 94.6% is against a naive lower()+substring filter — a common but weak design. It is not a claim about ML-based guards, which mostly resist these tricks (see guard-blindspots-tr).
  • normalize() is only as complete as its confusable map + NFKC. It folds the mappings it knows plus combining marks; homoglyphs outside the map (e.g. U+0261 "ɡ") pass through unchanged. The "100% caught" figure is on in-map evasions, not universal. Extend the map for your threat model, and pair it with semantic detection, sandboxing, and egress control.
  • The confusable map is a high-value subset, not the entire Unicode confusables table.

🔗 Related AltaySec work

Citation

@misc{yurtsevenler2026turkishcasefold,
  title  = {turkish-casefold-evasion: Bypassing Naive Prompt-Injection Filters with Turkish Case-Folding},
  author = {Yurtsevenler, Fevzi Ege},
  year   = {2026}, publisher = {AltaySec},
  howpublished = {\url{https://github.com/fevziegeyurtsevenler/turkish-casefold-evasion}}
}

Apache-2.0 · built by AltaySec — Türkçe-first AI/LLM security.

About

'İGNORE'.lower() != 'ignore'. Turkish case-folding & Unicode confusables bypass naive prompt-injection filters 94.6% of the time — measured, with dataset + a one-line NFKC fix.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages