loofah 2.23.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: 9817bf69ee1ad2d7e93edad53209db1347c9ec360076520532cdf60ad158a8f6
4
- data.tar.gz: 3381ac563f4b70c4ccca49301ba369f22716da405dd7641af535209e6817258b
3
+ metadata.gz: 4a721138b6b152897d72edddfb80cdb0f35200c39a8f37a2024b503e6cfacc95
4
+ data.tar.gz: b342436a5a4e544b504d3e4234935ecd09dd87eddb55be893a1105cfd6a627f6
5
5
  SHA512:
6
- metadata.gz: 482eeac3b61aba7e1b517aaab8a6d010ae44bd5b99b749cdabb5381936b4b9e8f97c62d99a60bdf1fbf64c2dba8c33062b676083f4e82d7e07f33e7666fe148f
7
- data.tar.gz: 69234fb9c4d7d55eb63a18008d035df1117bb6ea10f0f15b57bacc5152103fc0830a53a8d98a63e817360332975e257dbeda223ed43e42813724ce30a3b8d7e6
6
+ metadata.gz: e8ffd17d4b47d19bbff8070fcfe0d111530e7f146abc3501deaed1eb3198481f5ae16ee597e2a0d033eeea8bda27a038d1832c7237e24067a36e724e037e06a9
7
+ data.tar.gz: 71f741d1fbcf65fc137b25906c183cf767c091b203210e06d59dbb8e209c12274c77415b03406b021eb16dd149e2f9822f70124cec0947494f955d3fd1e458ee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
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
+
28
+ ## 2.24.1 / 2025-05-12
29
+
30
+ ### Ruby support
31
+
32
+ * Import only what's needed from `cgi` for support for Ruby 3.5 #296 @Earlopain
33
+
34
+
35
+ ## 2.24.0 / 2024-12-24
36
+
37
+ ### Added
38
+
39
+ * Built-in scrubber `:double_breakpoint` which sees `<br><br>` and wraps the surrounding content in `<p>` tags. #279, #284 @josecolella @torihuang
40
+
41
+ ### Improved
42
+
43
+ * Built-in scrubber `:targetblank` now skips `a` tags whose `href` attribute is an anchor link. Previously, all `a` tags were modified to have `target='_blank'`. #291 @fnando
44
+
45
+
3
46
  ## 2.23.1 / 2024-10-25
4
47
 
5
48
  ### Added
data/README.md CHANGED
@@ -31,6 +31,8 @@ Active Record extensions for HTML sanitization are available in the [`loofah-act
31
31
  * Add the _nofollow_ attribute to all hyperlinks.
32
32
  * Add the _target=\_blank_ attribute to all hyperlinks.
33
33
  * Remove _unprintable_ characters from text nodes.
34
+ * Some specialized HTML transformations are also built-in:
35
+ * Where `<br><br>` exists inside a `p` tag, close the `p` and open a new one.
34
36
  * Format markup as plain text, with (or without) sensible whitespace handling around block elements.
35
37
  * Replace Rails's `strip_tags` and `sanitize` view helper methods.
36
38
 
@@ -227,14 +229,15 @@ doc.scrub!(:whitewash) # removes unknown/unsafe/namespaced tags and their chi
227
229
  # and strips all node attributes
228
230
  ```
229
231
 
230
- Loofah also comes with some common transformation tasks:
232
+ Loofah also comes with built-in scrubers for some common transformation tasks:
231
233
 
232
234
  ``` ruby
233
- doc.scrub!(:nofollow) # adds rel="nofollow" attribute to links
234
- doc.scrub!(:noopener) # adds rel="noopener" attribute to links
235
- doc.scrub!(:noreferrer) # adds rel="noreferrer" attribute to links
236
- doc.scrub!(:unprintable) # removes unprintable characters from text nodes
237
- doc.scrub!(:targetblank) # adds target="_blank" attribute to links
235
+ doc.scrub!(:nofollow) # adds rel="nofollow" attribute to links
236
+ doc.scrub!(:noopener) # adds rel="noopener" attribute to links
237
+ doc.scrub!(:noreferrer) # adds rel="noreferrer" attribute to links
238
+ doc.scrub!(:unprintable) # removes unprintable characters from text nodes
239
+ doc.scrub!(:targetblank) # adds target="_blank" attribute to links
240
+ doc.scrub!(:double_breakpoint) # where `<br><br>` appears in a `p` tag, close the `p` and open a new one
238
241
  ```
239
242
 
240
243
  See `Loofah::Scrubbers` for more details and example usage.
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)|(%|&#37;)3A/i
986
+ PROTOCOL_SEPARATOR = /:|(&#0*58)|(&#x0*3a)|(%|&#37;)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",
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi"
3
+ require "cgi/escape"
4
+ require "cgi/util" if RUBY_VERSION < "3.5"
4
5
  require "crass"
5
6
 
6
7
  module Loofah
@@ -14,6 +15,43 @@ module Loofah
14
15
  CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES = /\A(["'])?[^"']+\1\z/
15
16
  DATA_ATTRIBUTE_NAME = /\Adata-[\w-]+\z/
16
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
+
17
55
  class << self
18
56
  def allowed_element?(element_name)
19
57
  ::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include?(element_name)
@@ -46,7 +84,7 @@ module Loofah
46
84
  end
47
85
 
48
86
  next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
49
- attr_name == "xlink:href" &&
87
+ SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) &&
50
88
  attr_node.value =~ /^\s*[^#\s].*/m
51
89
 
52
90
  attr_node.remove
@@ -139,23 +177,59 @@ module Loofah
139
177
  attr_node.value = values.join(" ")
140
178
  end
141
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
+
142
226
  def scrub_uri_attribute(attr_node)
143
- # this block lifted nearly verbatim from HTML5 sanitization
144
- val_unescaped = CGI.unescapeHTML(attr_node.value).gsub(CONTROL_CHARACTERS, "").downcase
145
- if val_unescaped =~ /^[a-z0-9][-+.a-z0-9]*:/ &&
146
- !SafeList::ALLOWED_PROTOCOLS.include?(val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[0])
227
+ if allowed_uri?(attr_node.value)
228
+ false
229
+ else
147
230
  attr_node.remove
148
- return true
149
- elsif val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[0] == "data"
150
- # permit only allowed data mediatypes
151
- mediatype = val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[1]
152
- mediatype, _ = mediatype.split(";")[0..1] if mediatype
153
- if mediatype && !SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(mediatype)
154
- attr_node.remove
155
- return true
156
- end
231
+ true
157
232
  end
158
- false
159
233
  end
160
234
 
161
235
  #
@@ -223,6 +297,20 @@ module Loofah
223
297
  string
224
298
  end
225
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
226
314
  end
227
315
  end
228
316
  end
@@ -251,7 +251,9 @@ module Loofah
251
251
  def scrub(node)
252
252
  return CONTINUE unless (node.type == Nokogiri::XML::Node::ELEMENT_NODE) && (node.name == "a")
253
253
 
254
- node.set_attribute("target", "_blank")
254
+ href = node["href"]
255
+
256
+ node.set_attribute("target", "_blank") if href && href[0] != "#"
255
257
 
256
258
  STOP
257
259
  end
@@ -348,6 +350,57 @@ module Loofah
348
350
  end
349
351
  end
350
352
 
353
+ #
354
+ # === scrub!(:double_breakpoint)
355
+ #
356
+ # +:double_breakpoint+ replaces double-break tags with closing/opening paragraph tags.
357
+ #
358
+ # markup = "<p>Some text here in a logical paragraph.<br><br>Some more text, apparently a second paragraph.</p>"
359
+ # Loofah.html5_fragment(markup).scrub!(:double_breakpoint)
360
+ # => "<p>Some text here in a logical paragraph.</p><p>Some more text, apparently a second paragraph.</p>"
361
+ #
362
+ class DoubleBreakpoint < Scrubber
363
+ def initialize # rubocop:disable Lint/MissingSuper
364
+ @direction = :top_down
365
+ end
366
+
367
+ def scrub(node)
368
+ return CONTINUE unless (node.type == Nokogiri::XML::Node::ELEMENT_NODE) && (node.name == "p")
369
+
370
+ paragraph_with_break_point_nodes = node.xpath("//p[br[following-sibling::br]]")
371
+
372
+ paragraph_with_break_point_nodes.each do |paragraph_node|
373
+ new_paragraph = paragraph_node.add_previous_sibling("<p>").first
374
+
375
+ paragraph_node.children.each do |child|
376
+ remove_blank_text_nodes(child)
377
+ end
378
+
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
390
+ end
391
+
392
+ paragraph_node.unlink
393
+ end
394
+
395
+ CONTINUE
396
+ end
397
+
398
+ private
399
+
400
+ def remove_blank_text_nodes(node)
401
+ node.unlink if node.text? && node.blank?
402
+ end
403
+ end
351
404
  #
352
405
  # A hash that maps a symbol (like +:prune+) to the appropriate Scrubber (Loofah::Scrubbers::Prune).
353
406
  #
@@ -362,6 +415,7 @@ module Loofah
362
415
  targetblank: TargetBlank,
363
416
  newline_block_elements: NewlineBlockElements,
364
417
  unprintable: Unprintable,
418
+ double_breakpoint: DoubleBreakpoint,
365
419
  }
366
420
 
367
421
  class << self
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Loofah
4
4
  # The version of Loofah you are using
5
- VERSION = "2.23.1"
5
+ VERSION = "2.25.2"
6
6
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loofah
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.23.1
4
+ version: 2.25.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
8
8
  - Bryan Helmkamp
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-10-25 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: crass
@@ -82,7 +81,7 @@ metadata:
82
81
  bug_tracker_uri: https://github.com/flavorjones/loofah/issues
83
82
  changelog_uri: https://github.com/flavorjones/loofah/blob/main/CHANGELOG.md
84
83
  documentation_uri: https://www.rubydoc.info/gems/loofah/
85
- post_install_message:
84
+ funding_uri: https://github.com/sponsors/flavorjones
86
85
  rdoc_options: []
87
86
  require_paths:
88
87
  - lib
@@ -97,8 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
96
  - !ruby/object:Gem::Version
98
97
  version: '0'
99
98
  requirements: []
100
- rubygems_version: 3.5.22
101
- signing_key:
99
+ rubygems_version: 4.0.10
102
100
  specification_version: 4
103
101
  summary: Loofah is a general library for manipulating and transforming HTML/XML documents
104
102
  and fragments, built on top of Nokogiri.