Module: SvgSentinel::Rules
Overview
The shared security vocabulary: which elements, attributes, and URI schemes are dangerous, plus the pure string helpers that read them. Both the Scanner (which reports) and the Sanitizer (which rewrites) mix this in, so the two can never drift out of agreement about what "dangerous" means.
Constant Summary collapse
- SCRIPT_ELEMENTS =
%w[script handler listener].freeze
- FOREIGN_ELEMENTS =
%w[foreignobject iframe embed object audio video canvas].freeze
- ANIMATION_ELEMENTS =
%w[animate animatecolor animatemotion animatetransform set mpath].freeze
- STRICT_ALLOWED_ELEMENTS =
The strict profile is an allowlist, not a blocklist: any element whose local name is not here (and is not already covered by a more specific rule) is reported. This fails closed - a new dangerous element is flagged simply for not being on the list. It is a conservative set of static, non-scripting, non-foreign SVG elements. Raster
, animation, and foreign/script elements are deliberately absent; they have their own, more descriptive findings. %w[ svg g defs symbol use switch view title desc metadata path rect circle ellipse line polyline polygon text tspan textpath tref altglyph glyphref lineargradient radialgradient stop solidcolor color-profile clippath mask pattern marker style font font-face missing-glyph glyph hkern vkern filter feblend fecolormatrix fecomponenttransfer fecomposite feconvolvematrix fediffuselighting fedisplacementmap fedropshadow feflood fegaussianblur femerge femergenode femorphology feoffset fespecularlighting fetile feturbulence fedistantlight fepointlight fespotlight fefunca fefuncr fefuncg fefuncb ].freeze
- DANGEROUS_SCHEMES =
%w[javascript vbscript livescript mocha].freeze
- URI_ATTRIBUTES =
%w[href src from to values by].freeze
- EXTERNAL_SCHEMES =
%w[http https ftp].freeze
- SENSITIVE_ANIMATED_ATTRIBUTES =
Attributes that a SMIL
/ must not target: writing script into an event handler, or a URI into href, is a script-injection vector regardless of the animation's tovalue. %w[href xlink:href style].freeze
- SCRIPTABLE_DATA_MEDIA =
data: media types that can carry executable markup. These are refused even when data URIs are otherwise allowed - "allow data URIs" means raster images, not an SVG or HTML document smuggled in as a data: URI.
%w[ image/svg+xml text/html application/xhtml+xml text/xml application/xml ].freeze
Class Method Summary collapse
- .allowed_element?(name) ⇒ Boolean
- .animation_element?(name) ⇒ Boolean
-
.classify_reference(target, allow_external: false, allow_data_uri: false) ⇒ Object
Classify a single reference target against the current allowances.
-
.css_reference_targets(css) ⇒ Object
Every off-document reference target in a chunk of CSS: both
url(...)and the string form of@import "..."(whichurl(...)scanning alone misses). -
.dangerous_css?(css, allow_external: false, allow_data_uri: false) ⇒ Boolean
True when this CSS text carries something executable or off-origin.
- .dangerous_scheme?(scheme) ⇒ Boolean
- .data_uri_media_type(value) ⇒ Object
-
.decontrol(str) ⇒ Object
Remove whitespace and C0 control characters (NUL through space), a common trick for hiding "javascript:" inside an attribute value.
- .event_handler_attr?(lname) ⇒ Boolean
- .external_scheme?(scheme) ⇒ Boolean
- .foreign_element?(name) ⇒ Boolean
- .script_element?(name) ⇒ Boolean
- .scriptable_data?(media) ⇒ Boolean
-
.sensitive_animation_target?(attribute_name) ⇒ Boolean
True when a SMIL animation's attributeName points at an attribute that can execute or fetch: an on* handler, href, or style.
- .strip_css_comments(css) ⇒ Object
-
.to_utf8(raw) ⇒ Object
Normalise arbitrary bytes to valid UTF-8 so downstream byte-level checks are meaningful and no encoding error can escape.
- .uri_attribute?(lname) ⇒ Boolean
-
.uri_scheme(value) ⇒ Object
Strip whitespace and control characters before reading a scheme, so "java\tscript:" and " javascript:" cannot slip past.
Class Method Details
.allowed_element?(name) ⇒ Boolean
66 67 68 |
# File 'lib/svg_sentinel/rules.rb', line 66 def allowed_element?(name) STRICT_ALLOWED_ELEMENTS.include?(name) end |
.animation_element?(name) ⇒ Boolean
62 63 64 |
# File 'lib/svg_sentinel/rules.rb', line 62 def animation_element?(name) ANIMATION_ELEMENTS.include?(name) end |
.classify_reference(target, allow_external: false, allow_data_uri: false) ⇒ Object
Classify a single reference target against the current allowances. Returns :dangerous (script/scriptable-data), :external, :data, or nil (relative/internal/allowed).
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/svg_sentinel/rules.rb', line 139 def classify_reference(target, allow_external: false, allow_data_uri: false) scheme = uri_scheme(target) return nil if scheme.nil? return :dangerous if dangerous_scheme?(scheme) if scheme == "data" return :dangerous if scriptable_data?(data_uri_media_type(target)) return allow_data_uri ? nil : :data end return :external if external_scheme?(scheme) && !allow_external nil end |
.css_reference_targets(css) ⇒ Object
Every off-document reference target in a chunk of CSS: both url(...)
and the string form of @import "..." (which url(...) scanning alone
misses). Yields each raw target string.
129 130 131 132 133 134 |
# File 'lib/svg_sentinel/rules.rb', line 129 def css_reference_targets(css) return enum_for(:css_reference_targets, css) unless block_given? css.scan(/url\(\s*(['"]?)(.*?)\1\s*\)/im) { |_quote, target| yield target } css.scan(/@import\s+(['"])(.*?)\1/im) { |_quote, target| yield target } end |
.dangerous_css?(css, allow_external: false, allow_data_uri: false) ⇒ Boolean
True when this CSS text carries something executable or off-origin. Used by the Scanner to report and by the Sanitizer to decide whether to drop a style declaration wholesale.
181 182 183 184 185 186 187 188 189 190 |
# File 'lib/svg_sentinel/rules.rb', line 181 def dangerous_css?(css, allow_external: false, allow_data_uri: false) raw = strip_css_comments(css.to_s) compact = decontrol(raw) return true if compact.match?(/expression\s*\(/i) return true if DANGEROUS_SCHEMES.any? { |s| compact.match?(/#{s}\s*:/i) } css_reference_targets(raw).any? do |target| !classify_reference(target, allow_external: allow_external, allow_data_uri: allow_data_uri).nil? end end |
.dangerous_scheme?(scheme) ⇒ Boolean
87 88 89 |
# File 'lib/svg_sentinel/rules.rb', line 87 def dangerous_scheme?(scheme) DANGEROUS_SCHEMES.include?(scheme) end |
.data_uri_media_type(value) ⇒ Object
107 108 109 110 111 112 113 114 |
# File 'lib/svg_sentinel/rules.rb', line 107 def data_uri_media_type(value) cleaned = decontrol(value.to_s) match = cleaned.match(/\Adata:([^;,]*)/i) return nil unless match type = match[1].downcase type.empty? ? "text/plain" : type end |
.decontrol(str) ⇒ Object
Remove whitespace and C0 control characters (NUL through space), a common trick for hiding "javascript:" inside an attribute value.
118 119 120 |
# File 'lib/svg_sentinel/rules.rb', line 118 def decontrol(str) str.gsub(/[[:cntrl:][:space:]]/, "") end |
.event_handler_attr?(lname) ⇒ Boolean
70 71 72 |
# File 'lib/svg_sentinel/rules.rb', line 70 def event_handler_attr?(lname) lname.start_with?("on") end |
.external_scheme?(scheme) ⇒ Boolean
91 92 93 |
# File 'lib/svg_sentinel/rules.rb', line 91 def external_scheme?(scheme) EXTERNAL_SCHEMES.include?(scheme) end |
.foreign_element?(name) ⇒ Boolean
58 59 60 |
# File 'lib/svg_sentinel/rules.rb', line 58 def foreign_element?(name) FOREIGN_ELEMENTS.include?(name) end |
.script_element?(name) ⇒ Boolean
54 55 56 |
# File 'lib/svg_sentinel/rules.rb', line 54 def script_element?(name) SCRIPT_ELEMENTS.include?(name) end |
.scriptable_data?(media) ⇒ Boolean
95 96 97 |
# File 'lib/svg_sentinel/rules.rb', line 95 def scriptable_data?(media) !media.nil? && SCRIPTABLE_DATA_MEDIA.include?(media) end |
.sensitive_animation_target?(attribute_name) ⇒ Boolean
True when a SMIL animation's attributeName points at an attribute that can execute or fetch: an on* handler, href, or style.
76 77 78 79 |
# File 'lib/svg_sentinel/rules.rb', line 76 def sensitive_animation_target?(attribute_name) target = decontrol(attribute_name.to_s).downcase event_handler_attr?(target) || SENSITIVE_ANIMATED_ATTRIBUTES.include?(target) end |
.strip_css_comments(css) ⇒ Object
122 123 124 |
# File 'lib/svg_sentinel/rules.rb', line 122 def strip_css_comments(css) css.gsub(%r{/\*.*?\*/}m, "") end |
.to_utf8(raw) ⇒ Object
Normalise arbitrary bytes to valid UTF-8 so downstream byte-level checks are meaningful and no encoding error can escape. Returns [:ok, string], [:bad_utf16, nil], or [:bad_utf8, nil].
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/svg_sentinel/rules.rb', line 158 def to_utf8(raw) str = raw.to_s bytes = str.b head = bytes[0, 2] if head == "\xFF\xFE".b || head == "\xFE\xFF".b src = (head == "\xFF\xFE".b) ? Encoding::UTF_16LE : Encoding::UTF_16BE begin return [:ok, bytes[2..].force_encoding(src).encode(Encoding::UTF_8)] rescue EncodingError return [:bad_utf16, nil] end end utf8 = str.dup.force_encoding(Encoding::UTF_8) return [:ok, utf8] if utf8.valid_encoding? [:bad_utf8, nil] end |
.uri_attribute?(lname) ⇒ Boolean
81 82 83 84 85 |
# File 'lib/svg_sentinel/rules.rb', line 81 def uri_attribute?(lname) return true if URI_ATTRIBUTES.include?(lname) lname.end_with?(":href") end |
.uri_scheme(value) ⇒ Object
Strip whitespace and control characters before reading a scheme, so "java\tscript:" and " javascript:" cannot slip past.
101 102 103 104 105 |
# File 'lib/svg_sentinel/rules.rb', line 101 def uri_scheme(value) cleaned = decontrol(value.to_s) match = cleaned.match(/\A([a-z][a-z0-9+.-]*):/i) match && match[1].downcase end |