Class: Vangrail::Rails::Obfuscation
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::Obfuscation
- Defined in:
- lib/vangrail/rails/obfuscation.rb
Overview
Runs other rails again over the text an attacker actually meant.
Every pattern rail reads what is written. An attacker who knows that writes it differently: base64 the paragraph and ask the model to decode it, rot13 it, spell it with Cyrillic letters that look like Latin ones, put zero-width joiners between the letters of "ignore", or set a right-to-left override so the rendered page and the byte sequence say different things. The model reads through all of it, because that is what models do, and the regexps see nothing.
The answer is not more patterns. It is to undo the encoding and run the rails that already exist over the result, which is why this takes a rail list rather than defining checks of its own:
Rails::Obfuscation.new(rails: [Rails::InjectedInstructions.new,
Rails::Jailbreak.new])
Each transform is applied on its own, and a variant identical to the original is dropped, so ordinary text costs one comparison per transform and nothing else. A hit names both the rail and the encoding it was hiding under, because "blocked" without that is unactionable for whoever has to look at the page.
Invisible characters are handled here directly rather than by a delegate: they are stripped, and the strip is reported as a rewrite. A zero-width joiner inside a word has no honest use in a handbook, and removing it costs a reader nothing while denying the cheapest bypass there is.
What this does not do is guess. There is no scoring, no entropy threshold, no "this looks encoded" heuristic that would fire on the base64 blobs and hashes a cluster handbook is full of. A blob either decodes to text a rail objects to, or it does not.
Constant Summary collapse
- INVISIBLE =
Zero-width and bidi control characters. The first four are the invisible separators; the bidi set is the trojan-source family, where the rendered order and the stored order disagree.
/[---]/- BASE64 =
A base64 run long enough to hold a sentence. Below this the decode is noise, and a handbook is full of short tokens that happen to be in the alphabet.
Bounded by lookaround rather than \b, because + and / are not word characters: a blob ending in one had its last character trimmed off the match, and a base64 string one character short decodes to a sentence with its tail missing. That cost the corpus a case, and the case it cost was an HTML comment, whose pattern needs the closing marker.
/(?<![A-Za-z0-9+\/=])[A-Za-z0-9+\/]{24,}={0,2}(?![A-Za-z0-9+\/=])/
Instance Attribute Summary collapse
-
#rails ⇒ Object
readonly
Returns the value of attribute rails.
-
#transforms ⇒ Object
readonly
Returns the value of attribute transforms.
Instance Method Summary collapse
- #cache_key(text, _context) ⇒ Object
- #call(text, context) ⇒ Object
-
#initialize(rails:, transforms: %i[invisible confusables confusables_all rot13 base64 nfkc],, name: 'obfuscation', sides: %i[input context])) ⇒ Obfuscation
constructor
A new instance of Obfuscation.
-
#offline? ⇒ Boolean
Only if everything it delegates to is.
-
#variants(text) ⇒ Object
The decoded forms of a text, labelled.
Constructor Details
#initialize(rails:, transforms: %i[invisible confusables confusables_all rot13 base64 nfkc],, name: 'obfuscation', sides: %i[input context])) ⇒ Obfuscation
Returns a new instance of Obfuscation.
59 60 61 62 63 64 |
# File 'lib/vangrail/rails/obfuscation.rb', line 59 def initialize(rails:, transforms: %i[invisible confusables confusables_all rot13 base64 nfkc], name: 'obfuscation', sides: %i[input context]) super(name: name, sides: sides) @rails = Array(rails) @transforms = Array(transforms).map(&:to_sym) end |
Instance Attribute Details
#rails ⇒ Object (readonly)
Returns the value of attribute rails.
57 58 59 |
# File 'lib/vangrail/rails/obfuscation.rb', line 57 def rails @rails end |
#transforms ⇒ Object (readonly)
Returns the value of attribute transforms.
57 58 59 |
# File 'lib/vangrail/rails/obfuscation.rb', line 57 def transforms @transforms end |
Instance Method Details
#cache_key(text, _context) ⇒ Object
72 73 74 |
# File 'lib/vangrail/rails/obfuscation.rb', line 72 def cache_key(text, _context) text if offline? end |
#call(text, context) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/vangrail/rails/obfuscation.rb', line 76 def call(text, context) body = text.to_s stripped = body.gsub(INVISIBLE, '') hit, uncertain = first_objection(body, stripped, context) return hit if hit return unchecked(uncertain.reason) if uncertain return pass if stripped == body modify(stripped, categories: ['invisible_characters'], reason: 'removed zero-width or bidi control characters') end |
#offline? ⇒ Boolean
Only if everything it delegates to is. A wrapper around a model rail inherits the model rail's posture.
68 69 70 |
# File 'lib/vangrail/rails/obfuscation.rb', line 68 def offline? rails.all?(&:offline?) end |
#variants(text) ⇒ Object
The decoded forms of a text, labelled. Public because an application that logs a blocked page wants to show what it decoded to.
92 93 94 95 96 97 98 99 100 |
# File 'lib/vangrail/rails/obfuscation.rb', line 92 def variants(text) body = text.to_s transforms.filter_map do |name| decoded = apply(name, body) next if decoded.nil? || decoded == body || decoded.strip.empty? [name, decoded] end end |