Class: Vangrail::Rails::Obfuscation

Inherits:
Vangrail::Rail show all
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+\/=])/

Constants inherited from Vangrail::Rail

Vangrail::Rail::DEFAULT_SIDES, Vangrail::Rail::SIDES

Instance Attribute Summary collapse

Attributes inherited from Vangrail::Rail

#name, #sides

Instance Method Summary collapse

Methods inherited from Vangrail::Rail

#applies_to?, #placeholder?, #to_s

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

#railsObject (readonly)

Returns the value of attribute rails.



57
58
59
# File 'lib/vangrail/rails/obfuscation.rb', line 57

def rails
  @rails
end

#transformsObject (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
# File 'lib/vangrail/rails/obfuscation.rb', line 76

def call(text, context)
  body = text.to_s
  stripped = body.gsub(INVISIBLE, '')

  hit = first_objection(body, stripped, context)
  return hit if hit

  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.

Returns:

  • (Boolean)


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.



91
92
93
94
95
96
97
98
99
# File 'lib/vangrail/rails/obfuscation.rb', line 91

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