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>
65 lines
2.0 KiB
Python
Executable File
65 lines
2.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""CASAN H4 — Suspicious base64/hex decoder (Track A, V4 encoding smuggling).
|
|
|
|
Reads text on stdin. Finds embedded base64 and hex blobs, decodes them, and
|
|
prints the decoded plaintext (one blob per line) on stdout so the caller can
|
|
re-run its injection/secret pattern scan on the *decoded* content.
|
|
|
|
Safety / no-false-positive design:
|
|
* Only blobs >= MIN_LEN characters are considered (short words are ignored).
|
|
* A decoded blob is emitted ONLY if it is mostly printable text. Random
|
|
base64-looking words (e.g. "objectives", DER key bytes) decode to
|
|
non-printable garbage and are dropped, so they can never trigger a match.
|
|
* Output is advisory: the caller decides a decoded blob is malicious only if
|
|
the decoded text itself matches a block/secret pattern.
|
|
|
|
Deterministic: same input always yields the same output.
|
|
"""
|
|
import base64
|
|
import binascii
|
|
import re
|
|
import sys
|
|
|
|
MIN_LEN = 16
|
|
PRINTABLE_RATIO = 0.8
|
|
|
|
B64_RE = re.compile(r"[A-Za-z0-9+/]{%d,}={0,2}" % MIN_LEN)
|
|
HEX_RE = re.compile(r"\b[0-9a-fA-F]{%d,}\b" % MIN_LEN)
|
|
|
|
|
|
def _mostly_printable(text: str) -> bool:
|
|
if not text:
|
|
return False
|
|
ok = sum(1 for c in text if c.isprintable() or c.isspace())
|
|
return ok >= PRINTABLE_RATIO * len(text)
|
|
|
|
|
|
def decode_blobs(data: str):
|
|
out = []
|
|
for m in B64_RE.findall(data):
|
|
pad = m + "=" * ((4 - len(m) % 4) % 4)
|
|
try:
|
|
dec = base64.b64decode(pad, validate=True)
|
|
except (binascii.Error, ValueError):
|
|
continue
|
|
txt = dec.decode("utf-8", "ignore")
|
|
if _mostly_printable(txt):
|
|
out.append(txt)
|
|
for m in HEX_RE.findall(data):
|
|
if len(m) % 2 != 0:
|
|
continue
|
|
try:
|
|
dec = bytes.fromhex(m)
|
|
except ValueError:
|
|
continue
|
|
txt = dec.decode("utf-8", "ignore")
|
|
if _mostly_printable(txt):
|
|
out.append(txt)
|
|
return out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
blobs = decode_blobs(sys.stdin.read())
|
|
if blobs:
|
|
sys.stdout.write("\n".join(blobs))
|