feat: obfuscation is no longer a bypass (#26) - #74
Merged
Conversation
Every pattern matched raw bytes, so a find-and-replace defeated the whole
library. All five evasions from the issue now land:
ignore-all-previous-instructions separator injection
i g n o r e a l l p r e v i o u s spacing
іgnore all previous instructions Cyrillic і homoglyph
ignore all previous instructions fullwidth
ig<U+200B>nore all previous instructions zero-width interleave
One pass beats all five, which is far more leverage than literal patterns per
spelling: an attacker has unbounded spellings and we do not.
Offsets are the whole problem. Normalizing is easy; reporting is not. Every byte
of normalized text carries the offset it came from, so findings name a real line
and QUOTE THE ORIGINAL — a user told their file contains "ignore all previous
instructions" when it visibly contains "ignore-all-previous-instructions" cannot
act on that.
Three bugs, all found by running it rather than reading it:
- The Unicode confusable table maps `m` onto `rn`, so the skeleton rewrote
"normal clean text" to "norrnal clean text". Every ASCII word in every
document, quietly mangled before matching. The skeleton now applies only to
non-ASCII, which is the only place it has a job: ASCII is the form it maps
toward.
- `\s` matches a newline, so scanning the whole normalized document let a
pattern span line breaks — quietly a second multi-line pass, but without the
paragraph boundaries #24 established. It joined across a blank list item
that #24 correctly refuses to cross. Now matched line by line.
- A trailing full stop counted as separator injection, so the pass ran on
essentially every document for nothing. A separator only counts when it sits
directly BETWEEN two word characters: the hyphens in `ignore-all-previous`
are load-bearing, the full stop ending "…about prompts." is punctuation.
Spacing rejoin is decided per LINE, not per word. Judging words separately left
"i g n o r e a l l" as "ignore a l l", because no per-word threshold is both
low enough for a three-letter word and high enough to leave "a b" in prose alone.
Measured on the ecosystem tree: ZERO new false positives. The six findings that
disappear are this repo's own pattern library — a `name:` field like
`ignore-previous-instructions` normalizes into the attack it names, which is
correct behaviour on any other document. They are suppressed with the tool's own
inline directives and still reported as suppressed, so nothing goes silent.
PERF-01 is unaffected: 500 clean files in 20ms, identical with and without,
because clean ASCII short-circuits before any work. This repo scans in 40ms
against the 200ms budget. Dense hyphen-heavy trees do pay — eight repos go 0.77s
to 1.67s — since a deep package path is separator-shaped and defeats the
short-circuit.
Documented limit: fully despaced text is not rejoined, because the result would
be `ignoreallprevious` and every pattern joins its words with `\s+`. Matching
that means rewriting the pattern set, not the input.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #26 (SCAN-05, engine E1). Completes Phase 3's detection work.
Every pattern matched raw bytes, so a find-and-replace defeated the whole library. All five evasions from the issue now land:
ignore-all-previous-instructionsi g n o r e a l l p r e v i o u sіgnore all previous instructions(Cyrillic і)ignore all previous instructionsig<U+200B>nore all previous instructionsOne pass beats all five — far more leverage than a literal pattern per spelling, since an attacker has unbounded spellings and we don't.
Offsets are the whole problem
Normalizing is easy; reporting is not. Every byte of normalized text carries the offset it came from, so findings name a real line and quote the original:
Telling a user their file contains
ignore all previous instructionswhen it visibly contains the hyphenated form is a quote they can't search for, about a file they're being asked to fix.Three bugs, all found by running it rather than reading it
The confusable table maps
montorn. The skeleton rewrote"normal clean text"→"norrnal clean text"— every ASCII word in every document, quietly mangled before matching. It now applies only to non-ASCII, which is the only place it has a job: ASCII is the form it maps toward.\smatches a newline. Scanning the whole normalized document let a pattern span line breaks — quietly making this a second multi-line pass, but without the paragraph boundaries #24 established. It joined across a blank list item that #24 correctly refuses to cross. Now matched line by line.A trailing full stop counted as separator injection, so the pass ran on essentially every document for nothing. A separator only counts between two word characters: the hyphens in
ignore-all-previousare load-bearing, the full stop ending "…about prompts." is punctuation.Measured
Zero new false positives on the ecosystem tree.
The six findings that disappear are this repo's own pattern library: a
name:field likeignore-previous-instructionsnormalizes into the attack it names. That's correct behaviour on any other document, so they're suppressed with the tool's own inline directives — and still reported as suppressed, so nothing goes silent.The last row is the honest cost: a deep package path is separator-shaped, so hyphen-heavy trees defeat the short-circuit.
Documented limit
Fully despaced text (
i g n o r e a l l p r e v i o u s, no double spaces) is not rejoined. The result would beignoreallprevious, and every pattern joins its words with\s+— matching that means rewriting the pattern set, not the input. Recorded rather than papered over.Spacing rejoin is also decided per line, not per word: judging words separately left
"i g n o r e a l l"as"ignore a l l", because no per-word threshold is both low enough for a three-letter word and high enough to leave"a b"in prose alone.Tests
10 new, one per evasion plus one per bug above. A corpus specimen guards the separator fold against ordinary hyphenated technical prose — and caught me quoting a payload in its own HTML comment header, which scores 1.0 by design. Full suite 22 binaries green, clippy clean.