sanitize 7.0.1 → 7.0.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: 694fdddd1e40857325f7fac61ee5679e6a9fca926413c1b035062ab7449b92c7
4
- data.tar.gz: 426f1b4ece2fe0514139b661ba77ca7b9a6196481a0759a3ac81a9f446996e3d
3
+ metadata.gz: 9dcbdd9a00a2f80ddded816eb9b52215365ae34ec23e99f0d31b2a2b11757b49
4
+ data.tar.gz: 38be6ac4c8215e558b4b1f4c8ae2eb570f6225a6473964b52ffbcb49c2806779
5
5
  SHA512:
6
- metadata.gz: 3e58a303bb8a1285fbc75ab93acfd896444dc125d3b9b13884868366720bc451e5c69409b9a852014394604bc932e0ffbeeb7c1b7192eb3cd0a673ec10a79df2
7
- data.tar.gz: 8a4b8ca81da4bc4bb070941fab4b08f8e6d36a10487d0d90820e762155fe5717acfbc919c2676650d490cd7d9cac82f7a2f4ec21bc0f373f837938a23a670d6d
6
+ metadata.gz: d802d1606fa2a8cb2056125d26092ef056146252c38ff55a086776c35eda10b70c63882f7b473656f4ea5cf18dfc9864e1e2bc6d257b204a1072568a5a65d440
7
+ data.tar.gz: 0f64bb78dd9c4b2dca863530c02a08559c36ed9ec761c0d3f3b1d81fef3b1eefe39e7632d0354eaf75b49b5e755c0e0191f92b6fb6a669255fd48bde2df332d7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to Sanitize are documented in this file. The format (since version 7.0.0) is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## 7.0.2 (2026-11-02)
6
+
7
+ ### Security
8
+
9
+ - MathML and SVG elements whose `<math>` or `<svg>` ancestor has been removed are now also removed (keeping their contents, subject to `:remove_contents`), even if they're allowed by the config or have been allowlisted by a transformer. Previously, if Sanitize removed a `<math>` or `<svg>` element but kept its contents, its descendants were kept even though they'd been parsed as foreign elements, and browsers would reparse them as HTML elements. Since some elements are parsed very differently in foreign content than in HTML, this could allow arbitrary HTML to be injected. For example, the contents of a `<textarea>`, `<title>`, `<xmp>`, `<noembed>`, or `<noframes>` element inside a MathML or SVG element are parsed as markup rather than text, so an attribute value, comment, or raw text element inside it like `<svg><textarea><!--</textarea><img src=x onerror=alert(1)>-->` could close the element early once it was reparsed as HTML. Similarly, browsers that use legacy `<select>` parsing rules would parse the contents of a raw text element inside a `<select>` element like `<math><select><mi><xmp><input><img src=x onerror=alert(1)></xmp>` as markup. This only affected custom configs that don't remove the contents of `<math>` or `<svg>` elements and allow one of the affected elements. Sanitize's default and built-in configs aren't affected. Thanks to Mike Dalessio (@flavorjones) for reporting this issue. (GHSA-599p-54q3-ff89)
10
+
11
+ - Elements and comments inside text content elements such as `<textarea>`, `<title>`, `<xmp>`, `<noembed>`, and `<noframes>` are now always removed, even if they're allowed by the config or have been allowlisted by a transformer. Only their text is kept, subject to `:remove_contents`. This provides defense in depth against the issue above. (GHSA-599p-54q3-ff89)
12
+
13
+ - Sanitize now requires Nokogiri 1.19.7 or higher. Nokogiri versions since 1.19.5 escape `<` and `>` in serialized attribute values to match browser behavior, which provides defense in depth against the issue above. (GHSA-599p-54q3-ff89)
14
+
15
+ - Elements inside `<select>`, `<option>`, and `<optgroup>` elements that browsers using legacy `<select>` parsing rules wouldn't allow there are now always removed (keeping their contents, subject to `:remove_contents`), even if they're allowed by the config or have been allowlisted by a transformer. Previously, if Sanitize removed a `<template>` element inside a `<select>` element but kept its contents, any raw text element inside the `<template>` element would end up inside the `<select>` element. Browsers that use legacy `<select>` parsing rules ignore the start tags of raw text elements inside a `<select>` element and parse their contents as markup, so input like `<select><template><xmp><input><img src=x onerror=alert(1)></xmp>` could be used to inject arbitrary HTML. This only affected custom configs that allow `<select>` and at least one raw text element such as `<style>`, `<xmp>`, `<noembed>`, `<noframes>`, or `<plaintext>`. Sanitize's default and built-in configs aren't affected. (GHSA-mg79-gw37-jxx7)
16
+
17
+ ### Changed
18
+
19
+ - Ruby 3.2.0 is now the oldest supported Ruby version, since Nokogiri 1.19.x requires Ruby 3.2.0 or higher. Ruby 3.1 reached end of life in March 2025.
20
+
5
21
  ## 7.0.1 (2026-10-21)
6
22
 
7
23
  ### Security
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  class Sanitize
6
4
  module Config
7
5
  # Deeply freezes and returns the given configuration Hash.
data/lib/sanitize/css.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "crass"
4
- require "set"
5
4
 
6
5
  class Sanitize
7
6
  class CSS
@@ -2,12 +2,48 @@
2
2
 
3
3
  class Sanitize
4
4
  module Transformers
5
+ # Names of elements whose content is parsed as text rather than markup when
6
+ # they're in the HTML namespace.
7
+ #
8
+ # An element with one of these names can contain markup if it's in a foreign
9
+ # namespace like MathML or SVG. If Sanitize removes the foreign ancestor but
10
+ # keeps the element, its serialized contents will be reparsed as text inside
11
+ # an HTML element, so a closing tag inside a comment or an unescaped raw
12
+ # text element could break out of the element and inject arbitrary markup.
13
+ # To prevent this, Sanitize removes all elements and comments inside these
14
+ # elements, leaving only text.
15
+ TEXT_CONTENT_ELEMENTS = Set.new(%w[
16
+ iframe
17
+ noembed
18
+ noframes
19
+ noscript
20
+ plaintext
21
+ script
22
+ style
23
+ textarea
24
+ title
25
+ xmp
26
+ ]).freeze
27
+
5
28
  CleanComment = lambda do |env|
6
29
  node = env[:node]
7
30
 
8
- if node.type == Nokogiri::XML::Node::COMMENT_NODE
9
- node.unlink unless env[:is_allowlisted]
31
+ return unless node.type == Nokogiri::XML::Node::COMMENT_NODE
32
+
33
+ # Comments inside text content elements are always removed, even if
34
+ # they're allowlisted, since they can never be serialized safely.
35
+ parent = node.parent
36
+
37
+ while parent&.element?
38
+ if TEXT_CONTENT_ELEMENTS.include?(parent.name.downcase)
39
+ node.unlink
40
+ return
41
+ end
42
+
43
+ parent = parent.parent
10
44
  end
45
+
46
+ node.unlink unless env[:config][:allow_comments] || env[:is_allowlisted]
11
47
  end
12
48
  end
13
49
  end
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  class Sanitize
6
4
  module Transformers
7
5
  class CleanElement
6
+ # Maps the namespace URI of each foreign element type supported by HTML to
7
+ # the name of its root element.
8
+ FOREIGN_NAMESPACE_ROOTS = {
9
+ "http://www.w3.org/1998/Math/MathML" => "math",
10
+ "http://www.w3.org/2000/svg" => "svg"
11
+ }.freeze
12
+
8
13
  # Matches a valid HTML5 data attribute name. The unicode ranges included
9
14
  # here are a conservative subset of the full range of characters that are
10
15
  # technically allowed, with the intent of matching the most common
@@ -20,6 +25,14 @@ class Sanitize
20
25
  # https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes
21
26
  REGEX_DATA_ATTR = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u
22
27
 
28
+ # Maps `<select>` and related elements to the names of the elements that
29
+ # browsers using legacy `<select>` parsing rules allow as their children.
30
+ SELECT_CONTENT_MODEL = {
31
+ "optgroup" => Set["option"].freeze,
32
+ "option" => Set.new.freeze,
33
+ "select" => Set["hr", "optgroup", "option"].freeze
34
+ }.freeze
35
+
23
36
  # Elements whose content is treated as unescaped text by HTML parsers.
24
37
  UNESCAPED_TEXT_ELEMENTS = Set.new(%w[
25
38
  iframe
@@ -90,9 +103,18 @@ class Sanitize
90
103
 
91
104
  def call(env)
92
105
  node = env[:node]
93
- return if node.type != Nokogiri::XML::Node::ELEMENT_NODE || env[:is_allowlisted]
106
+ return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
94
107
 
95
108
  name = env[:node_name]
109
+ parent = node.parent
110
+
111
+ # Some elements are always removed, even if they're allowlisted, since
112
+ # browsers would parse them differently than Nokogiri did.
113
+ force_remove = orphaned_foreign_element?(node, name, parent) ||
114
+ invalid_select_descendant?(name, parent) ||
115
+ text_content_element?(parent)
116
+
117
+ return if env[:is_allowlisted] && !force_remove
96
118
 
97
119
  # Delete any element that isn't in the config allowlist, unless the node
98
120
  # has already been deleted from the document.
@@ -100,7 +122,7 @@ class Sanitize
100
122
  # It's important that we not try to reparent the children of a node that
101
123
  # has already been deleted, since that seems to trigger a memory leak in
102
124
  # Nokogiri.
103
- unless @elements.include?(name) || node.parent.nil?
125
+ if (force_remove || !@elements.include?(name)) && !parent.nil?
104
126
  # Elements like br, div, p, etc. need to be replaced with whitespace
105
127
  # in order to preserve readability.
106
128
  if @whitespace_elements.include?(name)
@@ -245,6 +267,55 @@ class Sanitize
245
267
  node.unlink
246
268
  end
247
269
  end
270
+
271
+ private
272
+
273
+ # Returns `true` if _parent_ is an HTML `<select>`, `<option>`, or
274
+ # `<optgroup>` element and _name_ isn't an element that browsers allow
275
+ # inside it.
276
+ #
277
+ # Browsers that use legacy `<select>` parsing rules ignore the start tags
278
+ # of most elements inside a `<select>` element, including raw text
279
+ # elements like `<style>` and `<xmp>`, so these elements' contents would
280
+ # be parsed as markup. Nokogiri parses these elements the same way, but
281
+ # they can still end up inside a `<select>` element via a `<template>`
282
+ # element or foreign content that Sanitize removes. Since traversal is
283
+ # top-down, removing these elements ensures that only allowed elements
284
+ # and text remain inside `<select>` elements.
285
+ def invalid_select_descendant?(name, parent)
286
+ return false unless parent&.element? && SELECT_CONTENT_MODEL.key?(parent.name.downcase)
287
+ return false if FOREIGN_NAMESPACE_ROOTS.key?(parent.namespace&.href)
288
+
289
+ !SELECT_CONTENT_MODEL[parent.name.downcase].include?(name)
290
+ end
291
+
292
+ # Returns `true` if _node_ is a MathML or SVG element other than a
293
+ # `<math>` or `<svg>` element and its parent isn't a MathML or SVG element.
294
+ #
295
+ # This happens when Sanitize removes a `<math>` or `<svg>` element but
296
+ # keeps its contents. Nokogiri parsed these descendants as foreign
297
+ # elements, but browsers would reparse them as HTML elements, which may
298
+ # be parsed very differently. For example, the contents of a foreign
299
+ # `<textarea>` element are parsed as markup rather than as text. Since
300
+ # traversal is top-down, removing these elements ensures that no foreign
301
+ # element is kept without a foreign root.
302
+ def orphaned_foreign_element?(node, name, parent)
303
+ root_name = FOREIGN_NAMESPACE_ROOTS[node.namespace&.href]
304
+ return false if root_name.nil? || name == root_name
305
+
306
+ !(parent&.element? && FOREIGN_NAMESPACE_ROOTS.key?(parent.namespace&.href))
307
+ end
308
+
309
+ # Returns `true` if _parent_ is a text content element, such as
310
+ # `<textarea>` or `<xmp>`.
311
+ #
312
+ # Browsers parse the contents of these elements as text when they're in
313
+ # the HTML namespace, so any element inside one must have been parsed in
314
+ # a foreign namespace. Since traversal is top-down, removing these
315
+ # elements ensures that only text remains inside text content elements.
316
+ def text_content_element?(parent)
317
+ !!parent&.element? && TEXT_CONTENT_ELEMENTS.include?(parent.name.downcase)
318
+ end
248
319
  end
249
320
  end
250
321
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Sanitize
4
- VERSION = "7.0.1"
4
+ VERSION = "7.0.2"
5
5
  end
data/lib/sanitize.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri"
4
- require "set"
5
4
 
6
5
  require_relative "sanitize/version"
7
6
  require_relative "sanitize/config"
@@ -97,7 +96,7 @@ class Sanitize
97
96
  # Default transformers always run at the end of the chain, after any custom
98
97
  # transformers.
99
98
  @transformers << Transformers::CleanElement.new(@config)
100
- @transformers << Transformers::CleanComment unless @config[:allow_comments]
99
+ @transformers << Transformers::CleanComment
101
100
 
102
101
  if @config[:elements].include?("style")
103
102
  scss = Sanitize::CSS.new(config)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sanitize
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.1
4
+ version: 7.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 1.16.8
32
+ version: 1.19.7
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 1.16.8
39
+ version: 1.19.7
40
40
  description: |
41
41
  Sanitize is an allowlist-based HTML and CSS sanitizer. It removes all HTML
42
42
  and/or CSS from a string except the elements, attributes, and properties you
@@ -76,7 +76,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
76
76
  requirements:
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
- version: 3.1.0
79
+ version: 3.2.0
80
80
  required_rubygems_version: !ruby/object:Gem::Requirement
81
81
  requirements:
82
82
  - - ">="