loofah 2.25.1 → 2.25.2

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: 4a721138b6b152897d72edddfb80cdb0f35200c39a8f37a2024b503e6cfacc95
4
+ data.tar.gz: b342436a5a4e544b504d3e4234935ecd09dd87eddb55be893a1105cfd6a627f6
5
5
  SHA512:
6
- metadata.gz: 0562da17634578281969ad6ebab59608ca78048d46a0ef298a5f767e32a0c66d11d74c1edd89a9710c80b2a27cc745af434890f8a79f055748e213642084f0c2
7
- data.tar.gz: a39abf331334da243c9b608ab0a41fe5c08c0a41622433c86a3013a8aaaa204458afc7b99fe7d9042471f467433c6f4101f8f09adaddfb80fe091bf2541b6917
6
+ metadata.gz: e8ffd17d4b47d19bbff8070fcfe0d111530e7f146abc3501deaed1eb3198481f5ae16ee597e2a0d033eeea8bda27a038d1832c7237e24067a36e724e037e06a9
7
+ data.tar.gz: 71f741d1fbcf65fc137b25906c183cf767c091b203210e06d59dbb8e209c12274c77415b03406b021eb16dd149e2f9822f70124cec0947494f955d3fd1e458ee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.25.2 / 2026-07-15
4
+
5
+ ### Security
6
+
7
+ * 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
8
+ * Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes the named character references `	` and `
`, which `CGI.unescapeHTML` does not decode and browsers strip from URIs, and rejects schemes split by them (e.g. `java	script:alert(1)`). See [GHSA-8whx-365g-h9vv](https://github.com/flavorjones/loofah/security/advisories/GHSA-8whx-365g-h9vv). @flavorjones
9
+ * 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
10
+
11
+ ### Improved
12
+
13
+ * 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
14
+ * 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
15
+ * 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
16
+
17
+
3
18
  ## 2.25.1 / 2026-03-17
4
19
 
5
20
  * 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",
@@ -978,7 +983,7 @@ module Loofah
978
983
  "stroke-opacity",
979
984
  ])
980
985
 
981
- PROTOCOL_SEPARATOR = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|%)3A/i
986
+ PROTOCOL_SEPARATOR = /:|(&#0*58)|(&#x0*3a)|(%|%)3A/i
982
987
 
983
988
  ACCEPTABLE_PROTOCOLS = Set.new([
984
989
  "afs",
@@ -987,7 +992,6 @@ module Loofah
987
992
  "data",
988
993
  "ed2k",
989
994
  "fax",
990
- "feed",
991
995
  "ftp",
992
996
  "gopher",
993
997
  "http",
@@ -14,7 +14,43 @@ module Loofah
14
14
  CSS_WHITESPACE = " "
15
15
  CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES = /\A(["'])?[^"']+\1\z/
16
16
  DATA_ATTRIBUTE_NAME = /\Adata-[\w-]+\z/
17
- URI_PROTOCOL_REGEX = /\A[a-z][a-z0-9+\-.]*:/ # RFC 3986
17
+
18
+ # Decimal (`&#58`) or hexadecimal (`&#x3a`) form, with or without the trailing semicolon that
19
+ # CGI.unescapeHTML requires but browsers do not.
20
+ NUMERIC_CHARACTER_REFERENCE = /&#(x[0-9a-f]+|[0-9]+);?/i
21
+
22
+ # A scheme (RFC 3986) followed by a protocol separator. The separator must recognize the same
23
+ # encoded-colon forms as PROTOCOL_SEPARATOR, otherwise a scheme split by an encoded colon (for
24
+ # example "javascript&#58alert(1)") would not be recognized as having a scheme and would skip
25
+ # protocol validation.
26
+ URI_PROTOCOL_REGEX = /\A[a-z][a-z0-9+\-.]*#{SafeList::PROTOCOL_SEPARATOR}/
27
+
28
+ # Matches a valid MIME type "essence" (type "/" subtype, no parameters), used to
29
+ # decide whether a data: URI mediatype is well-formed; a non-match is not a valid
30
+ # MIME type, which the data: URL processor treats as text/plain. Specs:
31
+ #
32
+ # https://mimesniff.spec.whatwg.org/#valid-mime-type
33
+ # https://mimesniff.spec.whatwg.org/#mime-type-essence
34
+ # https://mimesniff.spec.whatwg.org/#http-token-code-point
35
+ #
36
+ # The character class below is the HTTP token set (tchar) from RFC 9110 section
37
+ # 5.6.2, https://www.rfc-editor.org/rfc/rfc9110#name-tokens :
38
+ #
39
+ # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
40
+ # / "_" / "`" / "|" / "~" / DIGIT / ALPHA
41
+ #
42
+ # ALPHA is written a-z, not a-zA-Z, because allowed_uri? downcases the input first.
43
+ DATA_URI_MEDIATYPE = %r{
44
+ \A
45
+ [a-z0-9!\#$%&'*+\-.^_`|~]+ # type: 1*tchar
46
+ / # "/" is not a tchar, so it is the sole delimiter
47
+ [a-z0-9!\#$%&'*+\-.^_`|~]+ # subtype: 1*tchar
48
+ \z
49
+ }x
50
+
51
+ # HTML5 named character references for whitespace that browsers strip from
52
+ # URIs. CGI.unescapeHTML does not decode these, so they are handled explicitly.
53
+ WHITESPACE_CHARACTER_REFERENCES = /&(Tab|NewLine);/
18
54
 
19
55
  class << self
20
56
  def allowed_element?(element_name)
@@ -48,7 +84,7 @@ module Loofah
48
84
  end
49
85
 
50
86
  next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
51
- attr_name == "xlink:href" &&
87
+ SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) &&
52
88
  attr_node.value =~ /^\s*[^#\s].*/m
53
89
 
54
90
  attr_node.remove
@@ -141,29 +177,52 @@ module Loofah
141
177
  attr_node.value = values.join(" ")
142
178
  end
143
179
 
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.
180
+ # Returns true if the given URI string is safe, false otherwise. This method can be used to
181
+ # validate URI attribute values without requiring a Nokogiri DOM node.
147
182
  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
183
+ # CGI.unescapeHTML decodes numeric references only when they carry a trailing semicolon, so
184
+ # also decode the semicolon-less ones, which browsers still decode and execute. Normalizing
185
+ # more aggressively than a browser only rejects more, which is safe. Control characters are
186
+ # stripped both before and after decoding, since decoding can produce them. That strip must
187
+ # precede WHITESPACE_CHARACTER_REFERENCES: removing a control character can reveal a named
188
+ # whitespace reference.
189
+ uri_string = decode_numeric_character_references(CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, "")))
190
+ uri_string.gsub!(CONTROL_CHARACTERS, "")
191
+ uri_string.gsub!(WHITESPACE_CHARACTER_REFERENCES, "")
192
+ uri_string.gsub!("&colon;", ":")
193
+ uri_string.downcase!
153
194
  if URI_PROTOCOL_REGEX.match?(uri_string)
154
195
  protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0]
155
196
  return false unless SafeList::ALLOWED_PROTOCOLS.include?(protocol)
156
197
 
157
198
  if protocol == "data"
158
199
  # 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)
200
+ return false unless SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(data_uri_mediatype(uri_string))
162
201
  end
163
202
  end
164
203
  true
165
204
  end
166
205
 
206
+ def decode_numeric_character_references(string)
207
+ string.gsub(NUMERIC_CHARACTER_REFERENCE) do |reference|
208
+ digits = ::Regexp.last_match(1)
209
+ hexadecimal = digits.start_with?("x", "X")
210
+ digits = digits[1..-1] if hexadecimal
211
+ significant_digits = digits.sub(/\A0+/, "")
212
+
213
+ # The largest code point is U+10FFFF: 7 decimal or 6 hexadecimal significant digits.
214
+ # Anything longer is out of range; skip it without building a large integer from it.
215
+ next reference if significant_digits.length > (hexadecimal ? 6 : 7)
216
+
217
+ codepoint = significant_digits.to_i(hexadecimal ? 16 : 10)
218
+ begin
219
+ codepoint.chr(Encoding::UTF_8)
220
+ rescue RangeError
221
+ reference
222
+ end
223
+ end
224
+ end
225
+
167
226
  def scrub_uri_attribute(attr_node)
168
227
  if allowed_uri?(attr_node.value)
169
228
  false
@@ -238,6 +297,20 @@ module Loofah
238
297
  string
239
298
  end
240
299
  end
300
+
301
+ private
302
+
303
+ # Returns the mediatype of a data: URI per RFC 2397, or nil when the
304
+ # required comma is absent. allowed_uri? entity-decodes, downcases, and
305
+ # strips control characters before calling this. An omitted or malformed
306
+ # mediatype resolves to "text/plain", matching the WHATWG data: URL processor.
307
+ def data_uri_mediatype(uri_string)
308
+ metadata, comma, _data = uri_string.delete_prefix("data:").partition(",")
309
+ return nil if comma.empty?
310
+
311
+ mediatype = metadata.delete_suffix(";base64").split(";", 2).first.to_s.strip
312
+ mediatype.match?(DATA_URI_MEDIATYPE) ? mediatype : "text/plain"
313
+ end
241
314
  end
242
315
  end
243
316
  end
@@ -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.2"
6
6
  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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -96,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
98
  requirements: []
99
- rubygems_version: 4.0.3
99
+ rubygems_version: 4.0.10
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.