module Loofah::HTML5::Scrub
Constants
- CONTROL_CHARACTERS
- CRASS_SEMICOLON
- CSS_IMPORTANT
- CSS_KEYWORDISH
- CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES
- CSS_WHITESPACE
- DATA_ATTRIBUTE_NAME
- DATA_URI_MEDIATYPE
Matches a valid MIME type “essence” (type “/” subtype, no parameters), used to decide whether a data: URI mediatype is well-formed; a non-match is not a valid MIME type, which the data: URL processor treats as text/plain. Specs:
https://mimesniff.spec.whatwg.org/#valid-mime-type https://mimesniff.spec.whatwg.org/#mime-type-essence https://mimesniff.spec.whatwg.org/#http-token-code-point
The character class below is the HTTP token set (tchar) from RFC 9110 section 5.6.2, www.rfc-editor.org/rfc/rfc9110#name-tokens :
tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHAALPHA is written a-z, not a-zA-Z, because allowed_uri? downcases the input first.
- NUMERIC_CHARACTER_REFERENCE
Decimal (`:`) or hexadecimal (`:`) form, with or without the trailing semicolon that CGI.unescapeHTML requires but browsers do not.
- TABLE_FOR_ESCAPE_HTML__
- URI_PROTOCOL_REGEX
A scheme (RFC 3986) followed by a protocol separator. The separator must recognize the same encoded-colon forms as PROTOCOL_SEPARATOR, otherwise a scheme split by an encoded colon (for example “javascript:alert(1)”) would not be recognized as having a scheme and would skip protocol validation.
- WHITESPACE_CHARACTER_REFERENCES
HTML5named character references for whitespace that browsers strip from URIs. CGI.unescapeHTML does not decode these, so they are handled explicitly.
Public Class Methods
# File lib/loofah/html5/scrub.rb, line 56 def allowed_element?(element_name) ::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include?(element_name) end
Returns true if the given URI string is safe, false otherwise. This method can be used to validate URI attribute values without requiring a Nokogiri DOM node.
# File lib/loofah/html5/scrub.rb, line 182 def allowed_uri?(uri_string) # CGI.unescapeHTML decodes numeric references only when they carry a trailing semicolon, so # also decode the semicolon-less ones, which browsers still decode and execute. Normalizing # more aggressively than a browser only rejects more, which is safe. Control characters are # stripped both before and after decoding, since decoding can produce them. That strip must # precede WHITESPACE_CHARACTER_REFERENCES: removing a control character can reveal a named # whitespace reference. uri_string = decode_numeric_character_references(CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, ""))) uri_string.gsub!(CONTROL_CHARACTERS, "") uri_string.gsub!(WHITESPACE_CHARACTER_REFERENCES, "") uri_string.gsub!(":", ":") uri_string.downcase! if URI_PROTOCOL_REGEX.match?(uri_string) protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0] return false unless SafeList::ALLOWED_PROTOCOLS.include?(protocol) if protocol == "data" # permit only allowed data mediatypes return false unless SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(data_uri_mediatype(uri_string)) end end true end
# File lib/loofah/html5/scrub.rb, line 266 def cdata_escape(node) escaped_text = escape_tags(node.text) if Nokogiri.jruby? node.document.create_text_node(escaped_text) else node.document.create_cdata(escaped_text) end end
# File lib/loofah/html5/scrub.rb, line 261 def cdata_needs_escaping?(node) # Nokogiri's HTML4 parser on JRuby doesn't flag the child of a `style` tag as cdata, but it acts that way node.cdata? || (Nokogiri.jruby? && node.text? && node.parent.name == "style") end
# File lib/loofah/html5/scrub.rb, line 206 def decode_numeric_character_references(string) string.gsub(NUMERIC_CHARACTER_REFERENCE) do |reference| digits = ::Regexp.last_match(1) hexadecimal = digits.start_with?("x", "X") digits = digits[1..-1] if hexadecimal significant_digits = digits.sub(/\A0+/, "") # The largest code point is U+10FFFF: 7 decimal or 6 hexadecimal significant digits. # Anything longer is out of range; skip it without building a large integer from it. next reference if significant_digits.length > (hexadecimal ? 6 : 7) codepoint = significant_digits.to_i(hexadecimal ? 16 : 10) begin codepoint.chr(Encoding::UTF_8) rescue RangeError reference end end end
libxml2 >= 2.9.2 fails to escape comments within some attributes.
see comments about CVE-2018-8048 within the tests for more information
# File lib/loofah/html5/scrub.rb, line 240 def force_correct_attribute_escaping!(node) return unless Nokogiri::VersionInfo.instance.libxml2? node.attribute_nodes.each do |attr_node| next unless LibxmlWorkarounds::BROKEN_ESCAPING_ATTRIBUTES.include?(attr_node.name) tag_name = LibxmlWorkarounds::BROKEN_ESCAPING_ATTRIBUTES_QUALIFYING_TAG[attr_node.name] next unless tag_name.nil? || tag_name == node.name # # this block is just like CGI.escape in Ruby 2.4, but # only encodes space and double-quote, to mimic # pre-2.9.2 behavior # encoding = attr_node.value.encoding attr_node.value = attr_node.value.gsub(/[ "]/) do |m| "%" + m.unpack("H2" * m.bytesize).join("%").upcase end.force_encoding(encoding) end end
# File lib/loofah/html5/scrub.rb, line 161 def scrub_attribute_that_allows_local_ref(attr_node) return unless attr_node.value nodes = Crass::Parser.new(attr_node.value).parse_component_values values = nodes.map do |node| case node[:node] when :url if node[:value].start_with?("#") node[:raw] end when :hash, :ident, :string node[:raw] end end.compact attr_node.value = values.join(" ") end
alternative implementation of the html5lib attribute scrubbing algorithm
# File lib/loofah/html5/scrub.rb, line 61 def scrub_attributes(node) node.attribute_nodes.each do |attr_node| attr_name = if attr_node.namespace "#{attr_node.namespace.prefix}:#{attr_node.node_name}" else attr_node.node_name end if DATA_ATTRIBUTE_NAME.match?(attr_name) next end unless SafeList::ALLOWED_ATTRIBUTES.include?(attr_name) attr_node.remove next end if SafeList::ATTR_VAL_IS_URI.include?(attr_name) next if scrub_uri_attribute(attr_node) end if SafeList::SVG_ATTR_VAL_ALLOWS_REF.include?(attr_name) scrub_attribute_that_allows_local_ref(attr_node) end next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) && SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) && attr_node.value =~ /^\s*[^#\s].*/m attr_node.remove next end scrub_css_attribute(node) node.attribute_nodes.each do |attr_node| if attr_node.value !~ /[^[:space:]]/ && attr_node.name !~ DATA_ATTRIBUTE_NAME node.remove_attribute(attr_node.name) end end force_correct_attribute_escaping!(node) end
# File lib/loofah/html5/scrub.rb, line 110 def scrub_css(style) url_flags = [:url, :bad_url] style_tree = Crass.parse_properties(style) sanitized_tree = [] style_tree.each do |node| next unless node[:node] == :property next if node[:children].any? do |child| url_flags.include?(child[:node]) end name = node[:name].downcase next unless SafeList::ALLOWED_CSS_PROPERTIES.include?(name) || SafeList::ALLOWED_SVG_PROPERTIES.include?(name) || SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first) value = node[:children].map do |child| case child[:node] when :whitespace CSS_WHITESPACE when :string if CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES.match?(child[:raw]) Crass::Parser.stringify(child) end when :function if SafeList::ALLOWED_CSS_FUNCTIONS.include?(child[:name].downcase) Crass::Parser.stringify(child) end when :ident keyword = child[:value] if !SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first) || SafeList::ALLOWED_CSS_KEYWORDS.include?(keyword) || (keyword =~ CSS_KEYWORDISH) keyword end else child[:raw] end end.compact.join.strip next if value.empty? value << CSS_WHITESPACE << CSS_IMPORTANT if node[:important] propstring = format("%s:%s", name, value) sanitized_node = Crass.parse_properties(propstring).first sanitized_tree << sanitized_node << CRASS_SEMICOLON end Crass::Parser.stringify(sanitized_tree) end
# File lib/loofah/html5/scrub.rb, line 105 def scrub_css_attribute(node) style = node.attributes["style"] style.value = scrub_css(style.value) if style end
# File lib/loofah/html5/scrub.rb, line 226 def scrub_uri_attribute(attr_node) if allowed_uri?(attr_node.value) false else attr_node.remove true end end
Private Class Methods
Returns the mediatype of a data: URI per RFC 2397, or nil when the required comma is absent. allowed_uri? entity-decodes, downcases, and strips control characters before calling this. An omitted or malformed mediatype resolves to “text/plain”, matching the WHATWG data: URL processor.
# File lib/loofah/html5/scrub.rb, line 307 def data_uri_mediatype(uri_string) metadata, comma, _data = uri_string.delete_prefix("data:").partition(",") return nil if comma.empty? mediatype = metadata.delete_suffix(";base64").split(";", 2).first.to_s.strip mediatype.match?(DATA_URI_MEDIATYPE) ? mediatype : "text/plain" end