Class: Vangrail::Rails::Hidden
- Inherits:
-
Vangrail::Rail
- Object
- Vangrail::Rail
- Vangrail::Rails::Hidden
- Defined in:
- lib/vangrail/rails/hidden.rb
Overview
Reads the parts of a page a human never sees.
The largest measurement of indirect injection in the wild found roughly seven in ten instances sitting in non-rendered HTML: comments, meta tags, attributes, elements styled invisible. That is the natural place to put one. A visible paragraph telling an assistant to ignore its instructions is a paragraph the page's own readers will notice and report; the same sentence in an alt attribute is read by the model and by nobody else.
So this rail does not judge text. It extracts the spans a reader cannot see and hands each to the rails that already know what an injection looks like:
Rails::Hidden.new(rails: [Rails::InjectedInstructions.new,
Rails::Jailbreak.new])
Delegating rather than pattern-matching keeps one definition of "an injection" in the codebase, and keeps this class about where text was found rather than what it says. A hidden span with ordinary content in it passes: pages carry meta descriptions and alt text for good reasons, and a rail that objected to invisible text as such would reject most of the web.
Only useful where documents arrive as HTML. A retrieval step that converts to markdown before storing has usually dropped most of these carriers already, which is a reason to run this at the fetch boundary rather than a reason to skip it: what the converter drops silently is exactly what nobody is looking at.
Constant Summary collapse
- CARRIERS =
Each entry pulls the readable part out of one carrier. Order is reporting order, so the named carrier is the first that matched rather than the last.
{ 'comment' => /<!--(.*?)-->/m, 'meta' => /<meta\b[^>]*?\bcontent\s*=\s*["']([^"']{12,})["'][^>]*>/i, 'alt_text' => /<[^>]+\balt\s*=\s*["']([^"']{12,})["'][^>]*>/i, 'title_attribute' => /<[^>]+\btitle\s*=\s*["']([^"']{12,})["'][^>]*>/i, 'aria_label' => /<[^>]+\baria-label\s*=\s*["']([^"']{12,})["'][^>]*>/i, 'data_attribute' => /<[^>]+\bdata-[\w-]+\s*=\s*["']([^"']{12,})["'][^>]*>/i, 'script' => /<script\b[^>]*>(.*?)<\/script>/mi, 'template' => /<(?:template|noscript)\b[^>]*>(.*?)<\/(?:template|noscript)>/mi, # An element that is present, rendered, and invisible. The three ways # that is written in a page an attacker controls: a display or # visibility rule, a zero size, and text the colour of its background. 'invisible_style' => / <[^>]*\bstyle\s*=\s*["'][^"']* (?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0| font-size\s*:\s*0|color\s*:\s*(?:\#f{3,6}|white|transparent)) [^"']*["'][^>]*>(.*?)<\/ /xmi, 'hidden_attribute' => /<(\w+)\b[^>]*\bhidden\b[^>]*>(.*?)<\/\1>/mi, # Markdown carries two of its own: a link title, and image alt text. 'link_title' => /\[[^\]]*\]\([^)\s]+\s+["']([^"']{12,})["']\)/, 'image_alt' => /!\[([^\]]{12,})\]\(/, }.freeze
Instance Attribute Summary collapse
-
#carriers ⇒ Object
readonly
Returns the value of attribute carriers.
-
#rails ⇒ Object
readonly
Returns the value of attribute rails.
Instance Method Summary collapse
- #cache_key(text, _context) ⇒ Object
- #call(text, context) ⇒ Object
-
#initialize(rails:, carriers: CARRIERS, name: 'hidden', sides: [:context]) ⇒ Hidden
constructor
A new instance of Hidden.
- #offline? ⇒ Boolean
-
#spans(text) ⇒ Object
Every hidden span, labelled by where it came from.
Constructor Details
#initialize(rails:, carriers: CARRIERS, name: 'hidden', sides: [:context]) ⇒ Hidden
Returns a new instance of Hidden.
65 66 67 68 69 |
# File 'lib/vangrail/rails/hidden.rb', line 65 def initialize(rails:, carriers: CARRIERS, name: 'hidden', sides: [:context]) super(name: name, sides: sides) @rails = Array(rails) @carriers = carriers end |
Instance Attribute Details
#carriers ⇒ Object (readonly)
Returns the value of attribute carriers.
63 64 65 |
# File 'lib/vangrail/rails/hidden.rb', line 63 def carriers @carriers end |
#rails ⇒ Object (readonly)
Returns the value of attribute rails.
63 64 65 |
# File 'lib/vangrail/rails/hidden.rb', line 63 def rails @rails end |
Instance Method Details
#cache_key(text, _context) ⇒ Object
75 76 77 |
# File 'lib/vangrail/rails/hidden.rb', line 75 def cache_key(text, _context) text if offline? end |
#call(text, context) ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/vangrail/rails/hidden.rb', line 79 def call(text, context) uncertain = nil spans(text).each do |carrier, span| rails.each do |rail| result = rail.call(span, context) if result.blocked? return block(categories: (result.categories || []) + ["hidden:#{carrier}"], reason: "#{result.reason} (hidden in #{carrier.tr('_', ' ')})") end uncertain ||= result unless result.certain? end end return unchecked(uncertain.reason) if uncertain pass end |
#offline? ⇒ Boolean
71 72 73 |
# File 'lib/vangrail/rails/hidden.rb', line 71 def offline? rails.all?(&:offline?) end |
#spans(text) ⇒ Object
Every hidden span, labelled by where it came from. Public because an application that rejected a page wants to show what was in it.
99 100 101 102 103 104 105 106 107 |
# File 'lib/vangrail/rails/hidden.rb', line 99 def spans(text) body = text.to_s carriers.flat_map do |carrier, pattern| body.scan(pattern).filter_map do |match| span = Array(match).compact.max_by(&:length).to_s.strip [carrier, span] unless span.empty? end end end |