loofah 2.24.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: 16850a48486ab3e9191ceff0a4fd6d768f82151049332ae162068f6712efccb8
4
- data.tar.gz: 6ccd67672b489120796711e08643cbaec9c88648622fc0c3a1ac013e49534b25
3
+ metadata.gz: 4a721138b6b152897d72edddfb80cdb0f35200c39a8f37a2024b503e6cfacc95
4
+ data.tar.gz: b342436a5a4e544b504d3e4234935ecd09dd87eddb55be893a1105cfd6a627f6
5
5
  SHA512:
6
- metadata.gz: b2a4f569f20365f63d548506946736a20ee195a3b4149228489c39f1d6fddf2fe9c774ded5d88d0d3bd547a00110b42ab37d582f8701a01eb2a047070cc2b440
7
- data.tar.gz: 2bca5a9c58d363251e8ca5b3803a57b73e51506e9d294e45d69d1fef376b658f31901a359315c6d60974d469047e78307e7cd33005884314e98bf9d2775bd36a
6
+ metadata.gz: e8ffd17d4b47d19bbff8070fcfe0d111530e7f146abc3501deaed1eb3198481f5ae16ee597e2a0d033eeea8bda27a038d1832c7237e24067a36e724e037e06a9
7
+ data.tar.gz: 71f741d1fbcf65fc137b25906c183cf767c091b203210e06d59dbb8e209c12274c77415b03406b021eb16dd149e2f9822f70124cec0947494f955d3fd1e458ee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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
+
18
+ ## 2.25.1 / 2026-03-17
19
+
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
21
+
22
+ ## 2.25.0 / 2025-12-15
23
+
24
+ * Extract `Loofah::HTML5::Scrub.allowed_uri?` which operates on a string. Previously this logic was coupled to the parsed tree in `.scrub_uri_attribute`. #300 @flavorjones
25
+ * Tightened up how entities and control characters are handled when detecting allowed URIs. #301 @flavorjones
26
+
27
+
3
28
  ## 2.24.1 / 2025-05-12
4
29
 
5
30
  ### Ruby support
data/SECURITY.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The Loofah core contributors take security very seriously and investigate all reported vulnerabilities.
4
4
 
5
- If you would like to report a vulnerablity or have a security concern regarding Loofah, please [report it via HackerOne](https://hackerone.com/loofah/reports/new).
5
+ If you would like to report a vulnerablity or have a security concern regarding Loofah, please [report it via Github](https://github.com/flavorjones/loofah/security).
6
6
 
7
7
  Your report will be acknowledged within 24 hours, and you'll receive a more detailed response within 72 hours indicating next steps in handling your report.
8
8
 
@@ -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",
@@ -15,6 +15,43 @@ module Loofah
15
15
  CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES = /\A(["'])?[^"']+\1\z/
16
16
  DATA_ATTRIBUTE_NAME = /\Adata-[\w-]+\z/
17
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);/
54
+
18
55
  class << self
19
56
  def allowed_element?(element_name)
20
57
  ::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include?(element_name)
@@ -47,7 +84,7 @@ module Loofah
47
84
  end
48
85
 
49
86
  next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
50
- attr_name == "xlink:href" &&
87
+ SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) &&
51
88
  attr_node.value =~ /^\s*[^#\s].*/m
52
89
 
53
90
  attr_node.remove
@@ -140,23 +177,59 @@ module Loofah
140
177
  attr_node.value = values.join(" ")
141
178
  end
142
179
 
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.
182
+ def allowed_uri?(uri_string)
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!
194
+ if URI_PROTOCOL_REGEX.match?(uri_string)
195
+ protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0]
196
+ return false unless SafeList::ALLOWED_PROTOCOLS.include?(protocol)
197
+
198
+ if protocol == "data"
199
+ # permit only allowed data mediatypes
200
+ return false unless SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(data_uri_mediatype(uri_string))
201
+ end
202
+ end
203
+ true
204
+ end
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
+
143
226
  def scrub_uri_attribute(attr_node)
144
- # this block lifted nearly verbatim from HTML5 sanitization
145
- val_unescaped = CGI.unescapeHTML(attr_node.value).gsub(CONTROL_CHARACTERS, "").downcase
146
- if val_unescaped =~ /^[a-z0-9][-+.a-z0-9]*:/ &&
147
- !SafeList::ALLOWED_PROTOCOLS.include?(val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[0])
227
+ if allowed_uri?(attr_node.value)
228
+ false
229
+ else
148
230
  attr_node.remove
149
- return true
150
- elsif val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[0] == "data"
151
- # permit only allowed data mediatypes
152
- mediatype = val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[1]
153
- mediatype, _ = mediatype.split(";")[0..1] if mediatype
154
- if mediatype && !SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(mediatype)
155
- attr_node.remove
156
- return true
157
- end
231
+ true
158
232
  end
159
- false
160
233
  end
161
234
 
162
235
  #
@@ -224,6 +297,20 @@ module Loofah
224
297
  string
225
298
  end
226
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
227
314
  end
228
315
  end
229
316
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Loofah
4
4
  # The version of Loofah you are using
5
- VERSION = "2.24.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.24.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: 3.6.8
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.