loofah 2.25.1 → 2.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d07f9d310067f14d41ee4661fd3cd831de5af3d1c60bbfc949ba57219636ee9f
4
- data.tar.gz: 0ea8df2bfb5396bcbcbec9b1ba3c7d1d0e852739be595efc4e27826226246eac
3
+ metadata.gz: ab3a16b00807f63098be8c67dbd785edea496daf292dc063299d84d25ae56133
4
+ data.tar.gz: d9ba643579b0190c1f66aa4cdfeb79ec5ade635032172c805be58bd0d67fc5fc
5
5
  SHA512:
6
- metadata.gz: 0562da17634578281969ad6ebab59608ca78048d46a0ef298a5f767e32a0c66d11d74c1edd89a9710c80b2a27cc745af434890f8a79f055748e213642084f0c2
7
- data.tar.gz: a39abf331334da243c9b608ab0a41fe5c08c0a41622433c86a3013a8aaaa204458afc7b99fe7d9042471f467433c6f4101f8f09adaddfb80fe091bf2541b6917
6
+ metadata.gz: b9b7dc5b9f7e3c0ac798a7e70f6257bb65827baadae3861d17cf0a24e1bf5cab48c909cd463ecc51ae5ba59acd91ca73c004faa7d1c489bbd0e91b7245132c79
7
+ data.tar.gz: 7e17e0f6e9cec7edd72c0f254aa406e510531297af9acc10714cd7277ec1641d48ab369a0bfeeef585104c314a79cdfb795f112e84591a5cf81f4cb8eac46486
data/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.25.3 / 2026-11-01
4
+
5
+ ### Security
6
+
7
+ * Ensure `Loofah::HTML5::Scrub.allowed_uri?` rejects schemes split by a numeric character reference without a semicolon that ends at a character `allowed_uri?` strips (e.g. `javascript&#x3a alert(1)`) or at another character reference (e.g. `javascript&#x3aalert(1)`). Character references are now decoded before anything is stripped, as browsers do. See [GHSA-p9wj-w8xq-vjx6](https://github.com/flavorjones/loofah/security/advisories/GHSA-p9wj-w8xq-vjx6). @flavorjones
8
+ * Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes the named character reference `&sol;`, which `CGI.unescapeHTML` does not decode and browsers read as `/`, and rejects `data:` URIs that disguise a disallowed mediatype with it (e.g. `data:text&sol;html,<script>alert(1)</script>`). Previously that mediatype was treated as malformed and fell back to the allowed `text/plain`. Only v2.25.2 is affected. See [GHSA-jcff-cj73-4w4f](https://github.com/flavorjones/loofah/security/advisories/GHSA-jcff-cj73-4w4f). @flavorjones
9
+ * Ensure `Loofah::HTML5::Scrub.scrub_css` does not decode CSS escapes in identifiers. Previously, escaped parentheses (e.g. `url\28 http://example.com/\29`) came out as a live `url()`, which could make the viewer's browser fetch an attacker-chosen URL. See [GHSA-m3x9-938q-5767](https://github.com/flavorjones/loofah/security/advisories/GHSA-m3x9-938q-5767). @flavorjones
10
+ * Ensure that `href` and `xlink:href` on SVG `textPath` and `mpath` elements are restricted to local (same-document) references. Previously `SVG_ALLOW_LOCAL_HREF` spelled `textPath` in lowercase, which never matched the name the HTML5 parser assigns inside `<svg>`, and omitted `mpath` entirely. See [GHSA-345x-3rq2-jfrm](https://github.com/flavorjones/loofah/security/advisories/GHSA-345x-3rq2-jfrm). @flavorjones
11
+ * Fix quadratic-time scrubbing of the `href` and `xlink:href` attributes of SVG elements. Only Ruby 3.1 and earlier are affected, because Ruby 3.2 introduced linear-time regexp matching. See [GHSA-mf2j-8f84-cc9q](https://github.com/flavorjones/loofah/security/advisories/GHSA-mf2j-8f84-cc9q). @flavorjones
12
+ * Ensure HTML5 scrubber output does not turn into new markup when a browser parses it again. The fix is in Nokogiri v1.19.5, which escapes `<` and `>` in attribute values as the HTML standard requires, so Loofah now requires Nokogiri >= 1.19.5. See [GHSA-h6v6-rg66-fqj4](https://github.com/flavorjones/loofah/security/advisories/GHSA-h6v6-rg66-fqj4). @flavorjones @hackvertor
13
+
14
+ ### Dependencies
15
+
16
+ * Loofah now requires Ruby >= 3.2, the minimum that Nokogiri v1.19.x supports. We don't like dropping Ruby versions in a patch release, and we're sorry for the inconvenience, but it's the only way to make sure everyone gets the security fix in [GHSA-h6v6-rg66-fqj4](https://github.com/flavorjones/loofah/security/advisories/GHSA-h6v6-rg66-fqj4).
17
+
18
+ ### Added
19
+
20
+ * `Loofah::HTML5::Scrub.prefixed_attribute_name` and `Loofah::HTML5::Scrub.scrub_attribute_value` expose the html5lib attribute name and value rules, so downstream consumers can apply them instead of carrying their own copy. @flavorjones
21
+
22
+ ### Fixed
23
+
24
+ * Fix `:double_breakpoint`, which left document fragments unchanged despite its documented example. The scrubber now changes only paragraphs inside the scrubbed node, and its run time on a document grows linearly, not quadratically, with the paragraph count. #309 @OskarEichler
25
+ * Fix `:whitewash`, which passed CDATA sections through unchanged. Markup inside a CDATA section in `Loofah.xml_fragment` or `Loofah.xml_document` survived scrubbing and became live elements if the output was later parsed as HTML. The scrubber now escapes CDATA content, as `:strip`, `:prune` and `:escape` already do. On JRuby, every sanitizer now emits escaped CDATA content as a CDATA section, as on CRuby, instead of a text node that double-escaped it. #313 @flavorjones (reported by @MegaManSec)
26
+ * Fix `:double_breakpoint`, which raised `NoMethodError` on a paragraph that ends with a `<br>` after a double break. #314 @flavorjones (reported by @MegaManSec)
27
+ * Fix `Loofah::HTML5::Scrub.allowed_uri?`, which raised `Encoding::CompatibilityError` on a string in an encoding other than UTF-8 and `ArgumentError` on invalid UTF-8. It now transcodes the input to UTF-8 and returns `false` when the input has an invalid byte sequence or no UTF-8 representation. #315 @flavorjones
28
+
29
+
30
+ ## 2.25.2 / 2026-07-15
31
+
32
+ ### Security
33
+
34
+ * Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes numeric character references without semicolons (e.g. `javascript&#58alert(1)`), which browsers decode and execute, and rejects schemes split by them. See [GHSA-5qhf-9phg-95m2](https://github.com/flavorjones/loofah/security/advisories/GHSA-5qhf-9phg-95m2). @flavorjones
35
+ * Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes the named character references `&Tab;` and `&NewLine;`, which `CGI.unescapeHTML` does not decode and browsers strip from URIs, and rejects schemes split by them (e.g. `java&Tab;script:alert(1)`). See [GHSA-8whx-365g-h9vv](https://github.com/flavorjones/loofah/security/advisories/GHSA-8whx-365g-h9vv). @flavorjones
36
+ * Ensure that both `href` and `xlink:href` attributes on SVG elements like `use` are restricted to local (same-document) references. Previously only `xlink:href` was restricted, allowing the SVG 2 `href` attribute to reference external documents. See [GHSA-9wjq-cp2p-hrgf](https://github.com/flavorjones/loofah/security/advisories/GHSA-9wjq-cp2p-hrgf). @flavorjones
37
+
38
+ ### Improved
39
+
40
+ * Harden `data:` URI mediatype parsing in `Loofah::HTML5::Scrub.allowed_uri?`. The mediatype is now parsed following the [WHATWG data: URL spec](https://fetch.spec.whatwg.org/#data-urls) and [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397) instead of simply being split on a colon. A `data:` URI with an omitted or malformed mediatype is now treated as `text/plain` and allowed, and one without the required comma is now rejected. #305 @flavorjones
41
+ * Remove `feed` from the default set of allowed protocols. The [feed URI scheme](https://en.wikipedia.org/wiki/Feed_URI_scheme) was never accepted as a standard protocol, and no major browser supports it. Removing it reduces the attack surface particularly for non-browser contexts. #304 @flavorjones
42
+ * Remove a vestigial `&#x70` alternative from `Loofah::HTML5::SafeList::PROTOCOL_SEPARATOR`. This appears to be an ancient typo dating back to pre-extraction Rails circa 2007. #305 @flavorjones
43
+
44
+
3
45
  ## 2.25.1 / 2026-03-17
4
46
 
5
47
  * Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes unescaped whitespace entities and rejects schemas containing them. See [GHSA-46fp-8f5p-pf2m](https://github.com/flavorjones/loofah/security/advisories/GHSA-46fp-8f5p-pf2m). #302 @flavorjones
@@ -605,6 +605,11 @@ module Loofah
605
605
  "stroke",
606
606
  ])
607
607
 
608
+ SVG_HREF_ATTRIBUTES = Set.new([
609
+ "xlink:href",
610
+ "href",
611
+ ])
612
+
608
613
  SVG_ALLOW_LOCAL_HREF = Set.new([
609
614
  "altGlyph",
610
615
  "animate",
@@ -618,7 +623,8 @@ module Loofah
618
623
  "pattern",
619
624
  "radialGradient",
620
625
  "set",
621
- "textpath",
626
+ "mpath",
627
+ "textPath",
622
628
  "tref",
623
629
  "use",
624
630
  ])
@@ -978,7 +984,7 @@ module Loofah
978
984
  "stroke-opacity",
979
985
  ])
980
986
 
981
- PROTOCOL_SEPARATOR = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
987
+ PROTOCOL_SEPARATOR = /:|(&#0*58)|(&#x0*3a)|(%|&#37;)3A/i
982
988
 
983
989
  ACCEPTABLE_PROTOCOLS = Set.new([
984
990
  "afs",
@@ -987,7 +993,6 @@ module Loofah
987
993
  "data",
988
994
  "ed2k",
989
995
  "fax",
990
- "feed",
991
996
  "ftp",
992
997
  "gopher",
993
998
  "http",
@@ -13,8 +13,54 @@ module Loofah
13
13
  CSS_IMPORTANT = "!important"
14
14
  CSS_WHITESPACE = " "
15
15
  CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES = /\A(["'])?[^"']+\1\z/
16
+ CSS_URLISH_NODES = [:url, :bad_url, :bad_string]
16
17
  DATA_ATTRIBUTE_NAME = /\Adata-[\w-]+\z/
17
- URI_PROTOCOL_REGEX = /\A[a-z][a-z0-9+\-.]*:/ # RFC 3986
18
+
19
+ # Decimal (`&#58`) or hexadecimal (`&#x3a`) form, with or without the trailing semicolon that
20
+ # CGI.unescapeHTML requires but browsers do not.
21
+ NUMERIC_CHARACTER_REFERENCE = /&#(x[0-9a-f]+|[0-9]+);?/i
22
+
23
+ # A scheme (RFC 3986) followed by a protocol separator. The separator must recognize the same
24
+ # encoded-colon forms as PROTOCOL_SEPARATOR, otherwise a scheme split by an encoded colon (for
25
+ # example "javascript&#58alert(1)") would not be recognized as having a scheme and would skip
26
+ # protocol validation.
27
+ URI_PROTOCOL_REGEX = /\A[a-z][a-z0-9+\-.]*#{SafeList::PROTOCOL_SEPARATOR}/
28
+
29
+ # Matches a valid MIME type "essence" (type "/" subtype, no parameters), used to
30
+ # decide whether a data: URI mediatype is well-formed; a non-match is not a valid
31
+ # MIME type, which the data: URL processor treats as text/plain. Specs:
32
+ #
33
+ # https://mimesniff.spec.whatwg.org/#valid-mime-type
34
+ # https://mimesniff.spec.whatwg.org/#mime-type-essence
35
+ # https://mimesniff.spec.whatwg.org/#http-token-code-point
36
+ #
37
+ # The character class below is the HTTP token set (tchar) from RFC 9110 section
38
+ # 5.6.2, https://www.rfc-editor.org/rfc/rfc9110#name-tokens :
39
+ #
40
+ # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
41
+ # / "_" / "`" / "|" / "~" / DIGIT / ALPHA
42
+ #
43
+ # ALPHA is written a-z, not a-zA-Z, because allowed_uri? downcases the input first.
44
+ DATA_URI_MEDIATYPE = %r{
45
+ \A
46
+ [a-z0-9!\#$%&'*+\-.^_`|~]+ # type: 1*tchar
47
+ / # "/" is not a tchar, so it is the sole delimiter
48
+ [a-z0-9!\#$%&'*+\-.^_`|~]+ # subtype: 1*tchar
49
+ \z
50
+ }x
51
+
52
+ # HTML5 named character references for whitespace that browsers strip from
53
+ # URIs. CGI.unescapeHTML does not decode these, so they are handled explicitly.
54
+ WHITESPACE_CHARACTER_REFERENCES = /&(Tab|NewLine);/
55
+
56
+ # HTML5 named character references for the delimiters that decide how a URI parses:
57
+ # ":" ends the scheme, and "/" separates the type and subtype of a data: mediatype.
58
+ # CGI.unescapeHTML does not decode these either. Together with
59
+ # WHITESPACE_CHARACTER_REFERENCES these are every named character reference that
60
+ # decodes to U+0009, U+000A, U+002F, or U+003A, so no other named reference can
61
+ # change how a URI parses.
62
+ DELIMITER_CHARACTER_REFERENCES = { "&colon;" => ":", "&sol;" => "/" }.freeze
63
+ DELIMITER_CHARACTER_REFERENCE = Regexp.union(DELIMITER_CHARACTER_REFERENCES.keys)
18
64
 
19
65
  class << self
20
66
  def allowed_element?(element_name)
@@ -24,11 +70,7 @@ module Loofah
24
70
  # alternative implementation of the html5lib attribute scrubbing algorithm
25
71
  def scrub_attributes(node)
26
72
  node.attribute_nodes.each do |attr_node|
27
- attr_name = if attr_node.namespace
28
- "#{attr_node.namespace.prefix}:#{attr_node.node_name}"
29
- else
30
- attr_node.node_name
31
- end
73
+ attr_name = prefixed_attribute_name(attr_node)
32
74
 
33
75
  if DATA_ATTRIBUTE_NAME.match?(attr_name)
34
76
  next
@@ -39,20 +81,7 @@ module Loofah
39
81
  next
40
82
  end
41
83
 
42
- if SafeList::ATTR_VAL_IS_URI.include?(attr_name)
43
- next if scrub_uri_attribute(attr_node)
44
- end
45
-
46
- if SafeList::SVG_ATTR_VAL_ALLOWS_REF.include?(attr_name)
47
- scrub_attribute_that_allows_local_ref(attr_node)
48
- end
49
-
50
- next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
51
- attr_name == "xlink:href" &&
52
- attr_node.value =~ /^\s*[^#\s].*/m
53
-
54
- attr_node.remove
55
- next
84
+ scrub_attribute_value(node, attr_node, attr_name)
56
85
  end
57
86
 
58
87
  scrub_css_attribute(node)
@@ -66,28 +95,58 @@ module Loofah
66
95
  force_correct_attribute_escaping!(node)
67
96
  end
68
97
 
98
+ # The name an attribute is matched by in the safelists, qualified by its namespace prefix.
99
+ def prefixed_attribute_name(attr_node)
100
+ if attr_node.namespace
101
+ "#{attr_node.namespace.prefix}:#{attr_node.node_name}"
102
+ else
103
+ attr_node.node_name
104
+ end
105
+ end
106
+
107
+ # Apply the html5lib value rules to an attribute that is already permitted by name.
108
+ # Returns true when `scrub_uri_attribute` removed the attribute, in which case the caller
109
+ # should stop scrubbing it.
110
+ def scrub_attribute_value(node, attr_node, attr_name = prefixed_attribute_name(attr_node))
111
+ if SafeList::ATTR_VAL_IS_URI.include?(attr_name)
112
+ return true if scrub_uri_attribute(attr_node)
113
+ end
114
+
115
+ if SafeList::SVG_ATTR_VAL_ALLOWS_REF.include?(attr_name)
116
+ scrub_attribute_that_allows_local_ref(attr_node)
117
+ end
118
+
119
+ # The whitespace run is `[^\S\n]*` rather than `\s*` so that it cannot cross a line
120
+ # boundary. With `\s*` the engine rescanned the entire run from every line start,
121
+ # which is quadratic in the length of an all-whitespace value.
122
+ if SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
123
+ SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) &&
124
+ attr_node.value =~ /^[^\S\n]*[^#\s]/
125
+ attr_node.remove
126
+ end
127
+
128
+ false
129
+ end
130
+
69
131
  def scrub_css_attribute(node)
70
132
  style = node.attributes["style"]
71
133
  style.value = scrub_css(style.value) if style
72
134
  end
73
135
 
74
136
  def scrub_css(style)
75
- url_flags = [:url, :bad_url]
76
137
  style_tree = Crass.parse_properties(style)
77
138
  sanitized_tree = []
78
139
 
79
140
  style_tree.each do |node|
80
141
  next unless node[:node] == :property
81
- next if node[:children].any? do |child|
82
- url_flags.include?(child[:node])
83
- end
142
+ next if node[:children].any? { |child| CSS_URLISH_NODES.include?(child[:node]) }
84
143
 
85
144
  name = node[:name].downcase
86
145
  next unless SafeList::ALLOWED_CSS_PROPERTIES.include?(name) ||
87
146
  SafeList::ALLOWED_SVG_PROPERTIES.include?(name) ||
88
147
  SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first)
89
148
 
90
- value = node[:children].map do |child|
149
+ value = node[:children].filter_map do |child|
91
150
  case child[:node]
92
151
  when :whitespace
93
152
  CSS_WHITESPACE
@@ -104,30 +163,55 @@ module Loofah
104
163
  if !SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first) ||
105
164
  SafeList::ALLOWED_CSS_KEYWORDS.include?(keyword) ||
106
165
  (keyword =~ CSS_KEYWORDISH)
107
- keyword
166
+ child[:raw]
108
167
  end
109
168
  else
110
169
  child[:raw]
111
170
  end
112
- end.compact.join.strip
171
+ end.join.strip
113
172
 
114
173
  next if value.empty?
115
174
 
116
175
  value << CSS_WHITESPACE << CSS_IMPORTANT if node[:important]
176
+ # Dropping tokens can join the survivors into new ones (`url/(a)*/(b)` becomes `url/*/`),
177
+ # so the rebuilt string may tokenize differently from the original and stringify
178
+ # differently again. Keep a property only if its rebuilt string survives a parse and
179
+ # stringify unchanged, so the tokens checked are exactly the tokens a browser will see.
117
180
  propstring = format("%s:%s", name, value)
118
181
  sanitized_node = Crass.parse_properties(propstring).first
182
+ next unless Crass::Parser.stringify(sanitized_node) == propstring
183
+ next if css_property_unsafe?(sanitized_node)
184
+
119
185
  sanitized_tree << sanitized_node << CRASS_SEMICOLON
120
186
  end
121
187
 
122
188
  Crass::Parser.stringify(sanitized_tree)
123
189
  end
124
190
 
191
+ # Dropping a token joins its neighbours, which can build a token that was not in the input:
192
+ # dropping the string from `url\28 "x"linear-gradient(red)` leaves one function token. So the
193
+ # rebuilt property is checked for functions and strings, which the input check never sees.
194
+ def css_property_unsafe?(node)
195
+ node[:children].any? do |child|
196
+ case child[:node]
197
+ when *CSS_URLISH_NODES
198
+ true
199
+ when :string
200
+ !CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES.match?(child[:raw])
201
+ when :function
202
+ !SafeList::ALLOWED_CSS_FUNCTIONS.include?(child[:name].downcase(:ascii))
203
+ else
204
+ false
205
+ end
206
+ end
207
+ end
208
+
125
209
  def scrub_attribute_that_allows_local_ref(attr_node)
126
210
  return unless attr_node.value
127
211
 
128
212
  nodes = Crass::Parser.new(attr_node.value).parse_component_values
129
213
 
130
- values = nodes.map do |node|
214
+ values = nodes.filter_map do |node|
131
215
  case node[:node]
132
216
  when :url
133
217
  if node[:value].start_with?("#")
@@ -136,34 +220,68 @@ module Loofah
136
220
  when :hash, :ident, :string
137
221
  node[:raw]
138
222
  end
139
- end.compact
223
+ end
140
224
 
141
225
  attr_node.value = values.join(" ")
142
226
  end
143
227
 
144
- # Returns true if the given URI string is safe, false otherwise.
145
- # This method can be used to validate URI attribute values without
146
- # requiring a Nokogiri DOM node.
228
+ # Returns true if the given URI string is safe, false otherwise. This method can be used to
229
+ # validate URI attribute values without requiring a Nokogiri DOM node.
147
230
  def allowed_uri?(uri_string)
148
- # Replace control characters both before and after unescaping.
149
- uri_string = CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, ""))
150
- .gsub(CONTROL_CHARACTERS, "")
151
- .gsub("&colon;", ":")
152
- .downcase
231
+ # The character-reference regexps are UTF-8, so the input has to be transcoded before it can
232
+ # be matched against them. String#encode is a no-op on a string already tagged UTF-8, so an
233
+ # invalid byte sequence has to be rejected separately.
234
+ return false unless uri_string.valid_encoding?
235
+
236
+ begin
237
+ uri_string = uri_string.encode(Encoding::UTF_8)
238
+ rescue EncodingError
239
+ return false
240
+ end
241
+
242
+ # Decode character references before stripping anything, as a browser does: an HTML parser
243
+ # decodes the attribute value, then the URL parser strips characters from the result. A
244
+ # semicolon-less numeric reference ends at the first character that cannot extend it, so
245
+ # stripping that character first would let the reference run on into the following text.
246
+ # CGI.unescapeHTML decodes numeric references only when they carry a trailing semicolon, so
247
+ # the semicolon-less ones are decoded separately.
248
+ uri_string = CGI.unescapeHTML(decode_numeric_character_references(uri_string))
249
+ uri_string.gsub!(CONTROL_CHARACTERS, "")
250
+ uri_string.gsub!(WHITESPACE_CHARACTER_REFERENCES, "")
251
+ uri_string.gsub!(DELIMITER_CHARACTER_REFERENCE, DELIMITER_CHARACTER_REFERENCES)
252
+ uri_string.downcase!
153
253
  if URI_PROTOCOL_REGEX.match?(uri_string)
154
254
  protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0]
155
255
  return false unless SafeList::ALLOWED_PROTOCOLS.include?(protocol)
156
256
 
157
257
  if protocol == "data"
158
258
  # permit only allowed data mediatypes
159
- mediatype = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[1]
160
- mediatype, _ = mediatype.split(/[;,]/)[0..1] if mediatype
161
- return false if mediatype && !SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(mediatype)
259
+ return false unless SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(data_uri_mediatype(uri_string))
162
260
  end
163
261
  end
164
262
  true
165
263
  end
166
264
 
265
+ def decode_numeric_character_references(string)
266
+ string.gsub(NUMERIC_CHARACTER_REFERENCE) do |reference|
267
+ digits = ::Regexp.last_match(1)
268
+ hexadecimal = digits.start_with?("x", "X")
269
+ digits = digits[1..-1] if hexadecimal
270
+ significant_digits = digits.sub(/\A0+/, "")
271
+
272
+ # The largest code point is U+10FFFF: 7 decimal or 6 hexadecimal significant digits.
273
+ # Anything longer is out of range; skip it without building a large integer from it.
274
+ next reference if significant_digits.length > (hexadecimal ? 6 : 7)
275
+
276
+ codepoint = significant_digits.to_i(hexadecimal ? 16 : 10)
277
+ begin
278
+ codepoint.chr(Encoding::UTF_8)
279
+ rescue RangeError
280
+ reference
281
+ end
282
+ end
283
+ end
284
+
167
285
  def scrub_uri_attribute(attr_node)
168
286
  if allowed_uri?(attr_node.value)
169
287
  false
@@ -206,10 +324,10 @@ module Loofah
206
324
 
207
325
  def cdata_escape(node)
208
326
  escaped_text = escape_tags(node.text)
209
- if Nokogiri.jruby?
210
- node.document.create_text_node(escaped_text)
211
- else
327
+ if node.cdata?
212
328
  node.document.create_cdata(escaped_text)
329
+ else
330
+ node.document.create_text_node(escaped_text)
213
331
  end
214
332
  end
215
333
 
@@ -238,6 +356,20 @@ module Loofah
238
356
  string
239
357
  end
240
358
  end
359
+
360
+ private
361
+
362
+ # Returns the mediatype of a data: URI per RFC 2397, or nil when the
363
+ # required comma is absent. allowed_uri? entity-decodes, downcases, and
364
+ # strips control characters before calling this. An omitted or malformed
365
+ # mediatype resolves to "text/plain", matching the WHATWG data: URL processor.
366
+ def data_uri_mediatype(uri_string)
367
+ metadata, comma, _data = uri_string.delete_prefix("data:").partition(",")
368
+ return nil if comma.empty?
369
+
370
+ mediatype = metadata.delete_suffix(";base64").split(";", 2).first.to_s.strip
371
+ mediatype.match?(DATA_URI_MEDIATYPE) ? mediatype : "text/plain"
372
+ end
241
373
  end
242
374
  end
243
375
  end
@@ -201,7 +201,11 @@ module Loofah
201
201
  return CONTINUE if node.namespaces.empty?
202
202
  end
203
203
  when Nokogiri::XML::Node::TEXT_NODE, Nokogiri::XML::Node::CDATA_SECTION_NODE
204
- return CONTINUE
204
+ if HTML5::Scrub.cdata_needs_escaping?(node)
205
+ node.before(HTML5::Scrub.cdata_escape(node))
206
+ else
207
+ return CONTINUE
208
+ end
205
209
  end
206
210
  node.remove
207
211
  STOP
@@ -361,37 +365,34 @@ module Loofah
361
365
  #
362
366
  class DoubleBreakpoint < Scrubber
363
367
  def initialize # rubocop:disable Lint/MissingSuper
364
- @direction = :top_down
368
+ # a top-down walk would empty a paragraph before reaching a paragraph nested in it
369
+ @direction = :bottom_up
365
370
  end
366
371
 
367
372
  def scrub(node)
368
373
  return CONTINUE unless (node.type == Nokogiri::XML::Node::ELEMENT_NODE) && (node.name == "p")
374
+ return CONTINUE unless node.at_xpath("./br[following-sibling::br]")
369
375
 
370
- paragraph_with_break_point_nodes = node.xpath("//p[br[following-sibling::br]]")
376
+ new_paragraph = node.add_previous_sibling("<p>").first
371
377
 
372
- paragraph_with_break_point_nodes.each do |paragraph_node|
373
- new_paragraph = paragraph_node.add_previous_sibling("<p>").first
378
+ node.children.each do |child|
379
+ remove_blank_text_nodes(child)
380
+ end
374
381
 
375
- paragraph_node.children.each do |child|
376
- remove_blank_text_nodes(child)
377
- end
382
+ node.children.each do |child|
383
+ # already unlinked
384
+ next if child.parent.nil?
378
385
 
379
- paragraph_node.children.each do |child|
380
- # already unlinked
381
- next if child.parent.nil?
382
-
383
- if child.name == "br" && child.next_sibling.name == "br"
384
- new_paragraph = paragraph_node.add_previous_sibling("<p>").first
385
- child.next_sibling.unlink
386
- child.unlink
387
- else
388
- child.parent = new_paragraph
389
- end
386
+ if child.name == "br" && child.next_sibling&.name == "br"
387
+ new_paragraph = node.add_previous_sibling("<p>").first
388
+ child.next_sibling.unlink
389
+ child.unlink
390
+ else
391
+ child.parent = new_paragraph
390
392
  end
391
-
392
- paragraph_node.unlink
393
393
  end
394
394
 
395
+ node.unlink
395
396
  CONTINUE
396
397
  end
397
398
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Loofah
4
4
  # The version of Loofah you are using
5
- VERSION = "2.25.1"
5
+ VERSION = "2.25.3"
6
6
  end
data/lib/loofah.rb CHANGED
@@ -5,13 +5,7 @@ require "nokogiri"
5
5
  module Loofah
6
6
  class << self
7
7
  def html5_support?
8
- # Note that Loofah can only support HTML5 in Nokogiri >= 1.14.0 because it requires the
9
- # subclassing fix from https://github.com/sparklemotion/nokogiri/pull/2534
10
- return @html5_support if defined? @html5_support
11
-
12
- @html5_support =
13
- Gem::Version.new(Nokogiri::VERSION) > Gem::Version.new("1.14.0") &&
14
- Nokogiri.uses_gumbo?
8
+ Nokogiri.uses_gumbo?
15
9
  end
16
10
  end
17
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loofah
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.25.1
4
+ version: 2.25.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.12.0
33
+ version: 1.19.5
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 1.12.0
40
+ version: 1.19.5
41
41
  description: |
42
42
  Loofah is a general library for manipulating and transforming HTML/XML documents and fragments,
43
43
  built on top of Nokogiri.
@@ -89,14 +89,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
89
  requirements:
90
90
  - - ">="
91
91
  - !ruby/object:Gem::Version
92
- version: 2.5.0
92
+ version: 3.2.0
93
93
  required_rubygems_version: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
98
  requirements: []
99
- rubygems_version: 4.0.3
99
+ rubygems_version: 4.0.20
100
100
  specification_version: 4
101
101
  summary: Loofah is a general library for manipulating and transforming HTML/XML documents
102
102
  and fragments, built on top of Nokogiri.