Files
CASAN/AINative_OKR_CASAN5/.specify/scripts/bash/unicode-normalize.py
T
thanhnvandClaude Opus 4.8 ac40c0b281 feat(h4-hardening): Plan-07 Track A — A1 strict semantic, A2 unicode/encoding, A3 tool-output scan
A1 (V1): CASAN_SECURITY_STRICT=1 makes semantic classification REQUIRED and
  fail-closed — model unavailable/no-verdict → BLOCK, never a silent SKIP.
  Non-strict CASAN_SEMANTIC_CLASSIFY=1 keeps regex verdict but logs
  SEMANTIC_SKIPPED loudly (sourced casan-log.sh). Default (no flags) unchanged.
A2 (V3/V4): unicode-normalize.py (NFKC + zero-width strip + Cyrillic/Greek
  homoglyph fold) and decode-suspicious.py (base64/hex decode + rescan, printable
  filter to avoid false positives) feed new match_either/secret_match haystacks.
  Blocks homoglyph, zero-width, fullwidth, base64/hex-smuggled injection & secrets.
A3 (V7): tool-output-scan.sh scans tool output for injection/secret before it
  re-enters model context; wrapper runs it after H6-exec (mode off|warn|block,
  strict→block). warn is default to preserve benign-draft behaviour.

Baseline preserved: run-casan4 35/35, adversarial 44/44.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:20:50 +09:00

63 lines
2.6 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""CASAN H4 — Unicode confusable / obfuscation normalizer (Track A, V3).
Reads text on stdin, writes a normalized variant on stdout used ONLY for
injection/jailbreak phrase matching (never for PII/secret redaction, so we
never widen what counts as a secret).
Deterministic transforms, in order:
1. NFKC compatibility normalization — folds fullwidth (ignore) and
other compatibility forms to their ASCII equivalents.
2. Strip zero-width / BOM / soft-hyphen formatting characters that split a
word so a blocklist never sees it (i·g·n·o·r·e).
3. Fold a fixed table of common Cyrillic/Greek homoglyphs to their Latin
lookalikes (іgnоrе -> ignore). NFKC does NOT do this — confusables are a
separate Unicode concern — so the table is explicit and auditable.
The table is intentionally small and covers the lookalikes actually used in
prompt-injection homoglyph attacks; extend it as new vectors appear.
"""
import sys
import unicodedata
# Zero-width, BOM, and invisible formatting code points.
ZERO_WIDTH = {
0x200B, # zero-width space
0x200C, # zero-width non-joiner
0x200D, # zero-width joiner
0x2060, # word joiner
0xFEFF, # BOM / zero-width no-break space
0x00AD, # soft hyphen
0x180E, # Mongolian vowel separator
0x2061, 0x2062, 0x2063, 0x2064, # invisible math operators
}
# Common Cyrillic / Greek homoglyphs -> Latin lookalike. Lowercase and
# uppercase both listed because matching is case-insensitive downstream but the
# fold must run before case handling to be safe.
CONFUSABLES = {
# Cyrillic lowercase
"а": "a", "е": "e", "о": "o", "р": "p", "с": "c", "у": "y", "х": "x",
"ѕ": "s", "і": "i", "ј": "j", "ԁ": "d", "һ": "h", "ӏ": "l", "п": "n",
"г": "r", "т": "t", "к": "k", "м": "m", "в": "b",
# Cyrillic uppercase
"А": "A", "В": "B", "Е": "E", "К": "K", "М": "M", "Н": "H", "О": "O",
"Р": "P", "С": "C", "Т": "T", "У": "Y", "Х": "X", "Ѕ": "S", "І": "I",
"Ј": "J",
# Greek
"α": "a", "ε": "e", "ο": "o", "ρ": "p", "τ": "t", "ν": "v", "κ": "k",
"Α": "A", "Β": "B", "Ε": "E", "Ζ": "Z", "Η": "H", "Ι": "I", "Κ": "K",
"Μ": "M", "Ν": "N", "Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X",
}
def normalize(text: str) -> str:
text = unicodedata.normalize("NFKC", text)
text = "".join(ch for ch in text if ord(ch) not in ZERO_WIDTH)
text = "".join(CONFUSABLES.get(ch, ch) for ch in text)
return text
if __name__ == "__main__":
sys.stdout.write(normalize(sys.stdin.read()))