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. A child that rewrites a span (a key in a comment) replaces that span inside the page. The rest of the page is still the page.
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, 'hidden_input' => / <input\b(?=[^>]*\btype\s*=\s*["']?hidden["']?) [^>]*\bvalue\s*=\s*["']([^"']{12,})["'] [^>]*> /xi, # Markdown carries two of its own: a link title, and image alt text. 'link_title' => /\[[^\]]*\]\([^)\s]+\s+["']([^"']{12,})["']\)/, 'image_alt' => /!\[([^\]]{12,})\]\(/, }.freeze
Constants inherited from Vangrail::Rail
Vangrail::Rail::DEFAULT_SIDES, Vangrail::Rail::SIDES
Instance Attribute Summary collapse
-
#carriers ⇒ Object
readonly
Returns the value of attribute carriers.
-
#rails ⇒ Object
readonly
Returns the value of attribute rails.
Attributes inherited from Vangrail::Rail
Instance Method Summary collapse
- #cache_key(text, _context) ⇒ Object
- #decide(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.
Methods inherited from Vangrail::Rail
#applies_to?, #call, #language_agnostic?, #placeholder?, #posture?, #quantifies?, #to_s, usable
Constructor Details
#initialize(rails:, carriers: CARRIERS, name: 'hidden', sides: [:context]) ⇒ Hidden
Returns a new instance of Hidden.
71 72 73 74 75 |
# File 'lib/vangrail/rails/hidden.rb', line 71 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.
69 70 71 |
# File 'lib/vangrail/rails/hidden.rb', line 69 def carriers @carriers end |
#rails ⇒ Object (readonly)
Returns the value of attribute rails.
69 70 71 |
# File 'lib/vangrail/rails/hidden.rb', line 69 def rails @rails end |
Instance Method Details
#cache_key(text, _context) ⇒ Object
81 82 83 |
# File 'lib/vangrail/rails/hidden.rb', line 81 def cache_key(text, _context) text if offline? end |
#decide(text, context) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/vangrail/rails/hidden.rb', line 85 def decide(text, context) rewritten = text.to_s modified = nil uncertain = nil blocked = catch(:blocked) do carriers.each do |carrier, pattern| rewritten = rewritten.gsub(pattern) do |full| span = captured_span(Regexp.last_match) next full if span.empty? current = span rails.each do |rail| result = rail.call(current, context) extra = ["hidden:#{carrier}"] if result.blocked? throw :blocked, wrapped_block(result, extra, carrier) end if result.modified? current = result.content.to_s modified = [result, extra, carrier] uncertain ||= result unless result.certain? elsif !result.certain? uncertain ||= result end end current == span ? full : full.sub(span) { current } end end nil end return blocked if blocked if modified result, extra, carrier = modified return modify(rewritten, categories: (result.categories || []) + extra, reason: "#{result.reason} (hidden in #{carrier.tr('_', ' ')})", certain: result.certain? && uncertain.nil?) end return unchecked(uncertain.reason) if uncertain pass end |
#offline? ⇒ Boolean
77 78 79 |
# File 'lib/vangrail/rails/hidden.rb', line 77 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.
132 133 134 135 136 137 138 139 140 |
# File 'lib/vangrail/rails/hidden.rb', line 132 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 |