sanitize 7.0.0 → 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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +10 -1
- data/lib/sanitize/config.rb +0 -2
- data/lib/sanitize/css.rb +156 -62
- data/lib/sanitize/transformers/clean_comment.rb +38 -2
- data/lib/sanitize/transformers/clean_element.rb +75 -5
- data/lib/sanitize/version.rb +1 -1
- data/lib/sanitize.rb +1 -2
- metadata +9 -21
- data/test/common.rb +0 -4
- data/test/test_clean_comment.rb +0 -48
- data/test/test_clean_css.rb +0 -68
- data/test/test_clean_doctype.rb +0 -72
- data/test/test_clean_element.rb +0 -537
- data/test/test_config.rb +0 -66
- data/test/test_malicious_css.rb +0 -56
- data/test/test_malicious_html.rb +0 -340
- data/test/test_parser.rb +0 -76
- data/test/test_sanitize.rb +0 -198
- data/test/test_sanitize_css.rb +0 -468
- data/test/test_transformers.rb +0 -228
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9dcbdd9a00a2f80ddded816eb9b52215365ae34ec23e99f0d31b2a2b11757b49
|
|
4
|
+
data.tar.gz: 38be6ac4c8215e558b4b1f4c8ae2eb570f6225a6473964b52ffbcb49c2806779
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d802d1606fa2a8cb2056125d26092ef056146252c38ff55a086776c35eda10b70c63882f7b473656f4ea5cf18dfc9864e1e2bc6d257b204a1072568a5a65d440
|
|
7
|
+
data.tar.gz: 0f64bb78dd9c4b2dca863530c02a08559c36ed9ec761c0d3f3b1d81fef3b1eefe39e7632d0354eaf75b49b5e755c0e0191f92b6fb6a669255fd48bde2df332d7
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
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
|
+
|
|
21
|
+
## 7.0.1 (2026-10-21)
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
|
|
25
|
+
- CSS properties that contain a `{}` block anywhere in their value are now removed. Browsers that support [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting) reparse an invalid declaration containing a `{}` block as a nested style rule, so a value like `color:hover{position:fixed}` could previously be used to smuggle disallowed properties and URLs past Sanitize in a stylesheet or `<style>` element. (GHSA-wgw9-qhqg-8hv5)
|
|
26
|
+
|
|
27
|
+
- The contents of at-rules such as `@media` and `@supports` that appear in a list of declarations (inside a style rule, inside an at-rule like `@font-face`, or in CSS sanitized with `Sanitize::CSS.properties`) are now sanitized as declarations rather than as rules, matching how browsers that support CSS nesting parse them. Previously, a stylesheet like `p{@media all{position:fixed;a{}}}` could be used to smuggle disallowed properties and URLs past Sanitize, since Sanitize treated `position:fixed;a` as a style rule's selector. The same was true of CSS sanitized with `Sanitize::CSS.properties` if the result was later embedded in a style rule. Style rules nested inside these at-rules are now removed, just as style rules nested directly inside other style rules already were. (GHSA-wgw9-qhqg-8hv5)
|
|
28
|
+
|
|
29
|
+
- CSS properties, style rules, and at-rules that contain blocks or functions nested more deeply than Crass's maximum nesting depth (25 levels by default) are now removed. (GHSA-wgw9-qhqg-8hv5)
|
|
30
|
+
|
|
31
|
+
- Hardened CSS sanitization against tokenizer differentials involving bad URLs. CSS properties that contain a bad URL anywhere inside a simple block (such as `[...]` or `(...)`) are now removed, as are style rules whose selectors contain a bad URL and at-rules whose preludes contain a bad URL. URLs inside simple blocks are now also validated against the configured protocol allowlist. Previously, a bug in Crass (fixed in Crass 1.0.8) could be exploited to smuggle CSS declarations, rules, or at-rules past Sanitize by hiding them in a bad URL in one of these locations.
|
|
32
|
+
|
|
33
|
+
- CSS style rules whose selectors contain a semicolon are now removed, as a defense in depth measure. A semicolon is never valid in a selector, but when a style rule appears where a browser that supports CSS nesting expects declarations, the browser treats everything before the semicolon as a declaration. This could be used to smuggle disallowed properties past `Sanitize::CSS.tree!` when it was given a list of declarations rather than a list of rules (though this isn't something Sanitize itself does).
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Removed an unused `require "cgi"` that caused a warning on Ruby 4.0, since the CGI library is no longer part of Ruby's standard library.
|
|
38
|
+
|
|
5
39
|
## 7.0.0 (2024-12-29)
|
|
6
40
|
|
|
7
41
|
Sanitize has no breaking API changes in this release, but the major version number has been incremented because we've dropped support for end-of-life versions of Ruby. As long as you're using Ruby 3.1.0 or later, this should be a painless upgrade!
|
data/README.md
CHANGED
|
@@ -12,10 +12,16 @@ Sanitize is based on the [Nokogiri HTML5 parser][nokogiri], which parses HTML th
|
|
|
12
12
|
[crass]:https://github.com/rgrove/crass
|
|
13
13
|
[nokogiri]:https://github.com/sparklemotion/nokogiri
|
|
14
14
|
|
|
15
|
+
> [!WARNING]
|
|
16
|
+
>
|
|
17
|
+
> Sanitize cannot fully sanitize the contents of `<math>` or `<svg>` elements. MathML and SVG elements are [foreign elements](https://html.spec.whatwg.org/multipage/syntax.html#foreign-elements) that don't follow normal HTML parsing rules.
|
|
18
|
+
>
|
|
19
|
+
> By default, Sanitize will remove all MathML and SVG elements. If you add MathML or SVG elements to a custom element allowlist, you may create a security vulnerability in your application.
|
|
20
|
+
|
|
15
21
|
## Links
|
|
16
22
|
|
|
17
23
|
* [Home](https://github.com/rgrove/sanitize/)
|
|
18
|
-
* [API Docs](https://rubydoc.info/
|
|
24
|
+
* [API Docs](https://rubydoc.info/gems/sanitize/Sanitize)
|
|
19
25
|
* [Issues](https://github.com/rgrove/sanitize/issues)
|
|
20
26
|
* [Release History](https://github.com/rgrove/sanitize/releases)
|
|
21
27
|
* [Online Demo](https://sanitize-web.fly.dev/)
|
|
@@ -136,6 +142,9 @@ Sanitize.fragment(html,
|
|
|
136
142
|
|
|
137
143
|
Sanitize will happily clean up a standalone CSS stylesheet or property string without needing to invoke the HTML parser.
|
|
138
144
|
|
|
145
|
+
> [!WARNING]
|
|
146
|
+
> This should only be used to sanitize CSS for use in a standalone CSS file. To safely sanitize CSS for use in an HTML context, such as in a `<style>` element or a `style` attribute, you must use `Sanitize.document()` or `Sanitize.fragment()` with an appropriate allowlist as described in the previous sections. Failure to do so may enable HTML injection attacks.
|
|
147
|
+
|
|
139
148
|
```ruby
|
|
140
149
|
css = %[
|
|
141
150
|
@import url(evil.css);
|
data/lib/sanitize/config.rb
CHANGED
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
|
|
@@ -66,6 +65,10 @@ class Sanitize
|
|
|
66
65
|
# tree = Crass.parse(css)
|
|
67
66
|
# Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED)
|
|
68
67
|
#
|
|
68
|
+
# The tree must be a list of rules, such as the tree returned by
|
|
69
|
+
# `Crass.parse`. To sanitize a list of declarations, such as the contents
|
|
70
|
+
# of an HTML `style` attribute, use {.properties}.
|
|
71
|
+
#
|
|
69
72
|
# @return [Array] Sanitized Crass CSS parse tree.
|
|
70
73
|
def self.tree!(tree, config = {})
|
|
71
74
|
new(config).tree!(tree)
|
|
@@ -82,6 +85,11 @@ class Sanitize
|
|
|
82
85
|
@at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
|
|
83
86
|
@at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
|
|
84
87
|
@import_url_validator = @config[:import_url_validator]
|
|
88
|
+
|
|
89
|
+
@parse_options = {
|
|
90
|
+
preserve_comments: @config[:allow_comments],
|
|
91
|
+
preserve_hacks: @config[:allow_hacks]
|
|
92
|
+
}.freeze
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
# Sanitizes inline CSS style properties.
|
|
@@ -96,11 +104,8 @@ class Sanitize
|
|
|
96
104
|
#
|
|
97
105
|
# @return [String] Sanitized CSS properties.
|
|
98
106
|
def properties(css)
|
|
99
|
-
tree = Crass.parse_properties(css,
|
|
100
|
-
|
|
101
|
-
preserve_hacks: @config[:allow_hacks])
|
|
102
|
-
|
|
103
|
-
tree!(tree)
|
|
107
|
+
tree = Crass.parse_properties(css, @parse_options)
|
|
108
|
+
sanitize_tree!(tree, declarations: true)
|
|
104
109
|
Crass::Parser.stringify(tree)
|
|
105
110
|
end
|
|
106
111
|
|
|
@@ -127,10 +132,7 @@ class Sanitize
|
|
|
127
132
|
#
|
|
128
133
|
# @return [String] Sanitized CSS stylesheet.
|
|
129
134
|
def stylesheet(css)
|
|
130
|
-
tree = Crass.parse(css,
|
|
131
|
-
preserve_comments: @config[:allow_comments],
|
|
132
|
-
preserve_hacks: @config[:allow_hacks])
|
|
133
|
-
|
|
135
|
+
tree = Crass.parse(css, @parse_options)
|
|
134
136
|
tree!(tree)
|
|
135
137
|
Crass::Parser.stringify(tree)
|
|
136
138
|
end
|
|
@@ -155,48 +157,13 @@ class Sanitize
|
|
|
155
157
|
#
|
|
156
158
|
# scss.tree!(tree)
|
|
157
159
|
#
|
|
160
|
+
# The tree must be a list of rules, such as the tree returned by
|
|
161
|
+
# `Crass.parse`. To sanitize a list of declarations, such as the contents
|
|
162
|
+
# of an HTML `style` attribute, use {#properties}.
|
|
163
|
+
#
|
|
158
164
|
# @return [Array] Sanitized Crass CSS parse tree.
|
|
159
165
|
def tree!(tree)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
tree.map! do |node|
|
|
163
|
-
next nil if node.nil?
|
|
164
|
-
|
|
165
|
-
case node[:node]
|
|
166
|
-
when :at_rule
|
|
167
|
-
preceded_by_property = false
|
|
168
|
-
next at_rule!(node)
|
|
169
|
-
|
|
170
|
-
when :comment
|
|
171
|
-
next node if @config[:allow_comments]
|
|
172
|
-
|
|
173
|
-
when :property
|
|
174
|
-
prop = property!(node)
|
|
175
|
-
preceded_by_property = !prop.nil?
|
|
176
|
-
next prop
|
|
177
|
-
|
|
178
|
-
when :semicolon
|
|
179
|
-
# Only preserve the semicolon if it was preceded by an allowlisted
|
|
180
|
-
# property. Otherwise, omit it in order to prevent redundant
|
|
181
|
-
# semicolons.
|
|
182
|
-
if preceded_by_property
|
|
183
|
-
preceded_by_property = false
|
|
184
|
-
next node
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
when :style_rule
|
|
188
|
-
preceded_by_property = false
|
|
189
|
-
tree!(node[:children])
|
|
190
|
-
next node
|
|
191
|
-
|
|
192
|
-
when :whitespace
|
|
193
|
-
next node
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
nil
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
tree
|
|
166
|
+
sanitize_tree!(tree, declarations: false)
|
|
200
167
|
end
|
|
201
168
|
|
|
202
169
|
# -- Protected Instance Methods --------------------------------------------
|
|
@@ -204,22 +171,33 @@ class Sanitize
|
|
|
204
171
|
|
|
205
172
|
# Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
|
|
206
173
|
# current config doesn't allow this at-rule.
|
|
207
|
-
|
|
174
|
+
#
|
|
175
|
+
# If _declarations_ is `true`, the at-rule appears in a list of
|
|
176
|
+
# declarations (such as the contents of a style rule) rather than in a list
|
|
177
|
+
# of rules.
|
|
178
|
+
def at_rule!(rule, declarations:)
|
|
208
179
|
name = rule[:name].downcase
|
|
209
180
|
|
|
181
|
+
return nil if contains_invalid_node?(rule[:prelude])
|
|
182
|
+
|
|
210
183
|
if @at_rules_with_styles.include?(name)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
184
|
+
# Browsers that support CSS nesting parse the block of an at-rule like
|
|
185
|
+
# `@media` that appears in a list of declarations as more declarations
|
|
186
|
+
# rather than as rules, so it must be sanitized as declarations.
|
|
187
|
+
# Otherwise a declaration like `position:fixed;a{}` would be parsed as
|
|
188
|
+
# a style rule with the selector `position:fixed;a` and would bypass
|
|
189
|
+
# the property allowlist.
|
|
190
|
+
styles = if declarations
|
|
191
|
+
Crass::Parser.parse_properties(rule[:block], @parse_options)
|
|
192
|
+
else
|
|
193
|
+
Crass::Parser.parse_rules(rule[:block], @parse_options)
|
|
194
|
+
end
|
|
214
195
|
|
|
215
|
-
rule[:block] =
|
|
196
|
+
rule[:block] = sanitize_tree!(styles, declarations: declarations)
|
|
216
197
|
|
|
217
198
|
elsif @at_rules_with_properties.include?(name)
|
|
218
|
-
props = Crass::Parser.parse_properties(rule[:block],
|
|
219
|
-
|
|
220
|
-
preserve_hacks: @config[:allow_hacks])
|
|
221
|
-
|
|
222
|
-
rule[:block] = tree!(props)
|
|
199
|
+
props = Crass::Parser.parse_properties(rule[:block], @parse_options)
|
|
200
|
+
rule[:block] = sanitize_tree!(props, declarations: true)
|
|
223
201
|
|
|
224
202
|
elsif @at_rules.include?(name)
|
|
225
203
|
return nil if name == "import" && !import_url_allowed?(rule)
|
|
@@ -231,6 +209,31 @@ class Sanitize
|
|
|
231
209
|
rule
|
|
232
210
|
end
|
|
233
211
|
|
|
212
|
+
# Returns `true` if the given array of Crass nodes contains an invalid node
|
|
213
|
+
# (see {#invalid_node?}) at any depth.
|
|
214
|
+
def contains_invalid_node?(nodes)
|
|
215
|
+
contains_node?(nodes) { |node| invalid_node?(node) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns `true` if the given array of Crass nodes contains a node at any
|
|
219
|
+
# depth for which the given block returns a truthy value.
|
|
220
|
+
def contains_node?(nodes)
|
|
221
|
+
return false unless Array === nodes
|
|
222
|
+
|
|
223
|
+
stack = nodes.dup
|
|
224
|
+
|
|
225
|
+
until stack.empty?
|
|
226
|
+
node = stack.pop
|
|
227
|
+
next unless Hash === node
|
|
228
|
+
return true if yield(node)
|
|
229
|
+
|
|
230
|
+
value = node[:value]
|
|
231
|
+
stack.concat(value) if Array === value
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
234
237
|
# Returns `true` if the given CSS function name is an image-related function
|
|
235
238
|
# that may contain image URLs that need to be validated.
|
|
236
239
|
def image_function?(name)
|
|
@@ -250,6 +253,22 @@ class Sanitize
|
|
|
250
253
|
@import_url_validator.call(import_url)
|
|
251
254
|
end
|
|
252
255
|
|
|
256
|
+
# Returns `true` if the given Crass node is a `:bad_url` or `:error` node.
|
|
257
|
+
#
|
|
258
|
+
# A bad URL is invalid CSS that a browser will discard, but a parser that
|
|
259
|
+
# disagrees with the browser about where a bad URL ends could be tricked
|
|
260
|
+
# into hiding CSS inside it.
|
|
261
|
+
#
|
|
262
|
+
# An `:error` node is CSS that Crass couldn't or wouldn't parse, such as a
|
|
263
|
+
# block nested more deeply than Crass's maximum nesting depth. Its contents
|
|
264
|
+
# are never inspected, but they're preserved verbatim when the tree is
|
|
265
|
+
# stringified, so a browser would still see them.
|
|
266
|
+
#
|
|
267
|
+
# It's safest to reject any construct that contains either one.
|
|
268
|
+
def invalid_node?(node)
|
|
269
|
+
node[:node] == :bad_url || node[:node] == :error
|
|
270
|
+
end
|
|
271
|
+
|
|
253
272
|
# Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
|
|
254
273
|
# current config doesn't allow this property.
|
|
255
274
|
def property!(prop)
|
|
@@ -266,6 +285,9 @@ class Sanitize
|
|
|
266
285
|
combined_value = +""
|
|
267
286
|
|
|
268
287
|
nodes.each do |child|
|
|
288
|
+
# Reject properties that contain an invalid node at any depth.
|
|
289
|
+
return nil if invalid_node?(child)
|
|
290
|
+
|
|
269
291
|
value = child[:value]
|
|
270
292
|
|
|
271
293
|
case child[:node]
|
|
@@ -299,14 +321,86 @@ class Sanitize
|
|
|
299
321
|
when :url
|
|
300
322
|
return nil unless valid_url?(child)
|
|
301
323
|
|
|
302
|
-
when :
|
|
303
|
-
|
|
324
|
+
when :simple_block
|
|
325
|
+
# Reject properties that contain a `{}` block at any depth.
|
|
326
|
+
#
|
|
327
|
+
# A `{}` block isn't valid in a standard property value, and browsers
|
|
328
|
+
# that support CSS nesting will reparse an invalid declaration that
|
|
329
|
+
# contains one as a nested style rule (e.g.
|
|
330
|
+
# `color:hover{position:fixed}` becomes the rule
|
|
331
|
+
# `color:hover { position: fixed }`), which would allow disallowed
|
|
332
|
+
# properties to be smuggled past the sanitizer.
|
|
333
|
+
return nil if child[:start] == "{"
|
|
334
|
+
|
|
335
|
+
nodes.concat(value) if Array === value
|
|
304
336
|
end
|
|
305
337
|
end
|
|
306
338
|
|
|
307
339
|
prop
|
|
308
340
|
end
|
|
309
341
|
|
|
342
|
+
# Sanitizes the given Crass CSS parse tree and all its children, modifying
|
|
343
|
+
# it in place. See {#tree!}.
|
|
344
|
+
#
|
|
345
|
+
# If _declarations_ is `true`, the tree is a list of declarations (such as
|
|
346
|
+
# the contents of a style rule) rather than a list of rules.
|
|
347
|
+
def sanitize_tree!(tree, declarations:)
|
|
348
|
+
preceded_by_property = false
|
|
349
|
+
|
|
350
|
+
tree.map! do |node|
|
|
351
|
+
next nil if node.nil?
|
|
352
|
+
|
|
353
|
+
case node[:node]
|
|
354
|
+
when :at_rule
|
|
355
|
+
preceded_by_property = false
|
|
356
|
+
next at_rule!(node, declarations: declarations)
|
|
357
|
+
|
|
358
|
+
when :comment
|
|
359
|
+
next node if @config[:allow_comments]
|
|
360
|
+
|
|
361
|
+
when :property
|
|
362
|
+
prop = property!(node)
|
|
363
|
+
preceded_by_property = !prop.nil?
|
|
364
|
+
next prop
|
|
365
|
+
|
|
366
|
+
when :semicolon
|
|
367
|
+
# Only preserve the semicolon if it was preceded by an allowlisted
|
|
368
|
+
# property. Otherwise, omit it in order to prevent redundant
|
|
369
|
+
# semicolons.
|
|
370
|
+
if preceded_by_property
|
|
371
|
+
preceded_by_property = false
|
|
372
|
+
next node
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
when :style_rule
|
|
376
|
+
preceded_by_property = false
|
|
377
|
+
|
|
378
|
+
# Reject style rules whose selectors contain an invalid node (see
|
|
379
|
+
# {#invalid_node?}) or a semicolon at any depth.
|
|
380
|
+
#
|
|
381
|
+
# A semicolon is never valid in a selector, but if a style rule
|
|
382
|
+
# appears where a browser expects declarations (such as the contents
|
|
383
|
+
# of an `@media` rule in a list of declarations), the browser will
|
|
384
|
+
# treat everything before the semicolon as a declaration. For
|
|
385
|
+
# example, `position:fixed;a{}` would be treated as the declaration
|
|
386
|
+
# `position:fixed` followed by the style rule `a{}`.
|
|
387
|
+
next nil if contains_node?(node.dig(:selector, :tokens)) { |token|
|
|
388
|
+
invalid_node?(token) || token[:node] == :semicolon
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
sanitize_tree!(node[:children], declarations: true)
|
|
392
|
+
next node
|
|
393
|
+
|
|
394
|
+
when :whitespace
|
|
395
|
+
next node
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
nil
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
tree
|
|
402
|
+
end
|
|
403
|
+
|
|
310
404
|
# Returns `true` if the given node (which may be of type `:url` or
|
|
311
405
|
# `:function`, since the CSS syntax can produce both) uses an allowlisted
|
|
312
406
|
# protocol.
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "cgi"
|
|
4
|
-
require "set"
|
|
5
|
-
|
|
6
3
|
class Sanitize
|
|
7
4
|
module Transformers
|
|
8
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
|
+
|
|
9
13
|
# Matches a valid HTML5 data attribute name. The unicode ranges included
|
|
10
14
|
# here are a conservative subset of the full range of characters that are
|
|
11
15
|
# technically allowed, with the intent of matching the most common
|
|
@@ -21,6 +25,14 @@ class Sanitize
|
|
|
21
25
|
# https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes
|
|
22
26
|
REGEX_DATA_ATTR = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u
|
|
23
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
|
+
|
|
24
36
|
# Elements whose content is treated as unescaped text by HTML parsers.
|
|
25
37
|
UNESCAPED_TEXT_ELEMENTS = Set.new(%w[
|
|
26
38
|
iframe
|
|
@@ -91,9 +103,18 @@ class Sanitize
|
|
|
91
103
|
|
|
92
104
|
def call(env)
|
|
93
105
|
node = env[:node]
|
|
94
|
-
return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
|
|
106
|
+
return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
|
|
95
107
|
|
|
96
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
|
|
97
118
|
|
|
98
119
|
# Delete any element that isn't in the config allowlist, unless the node
|
|
99
120
|
# has already been deleted from the document.
|
|
@@ -101,7 +122,7 @@ class Sanitize
|
|
|
101
122
|
# It's important that we not try to reparent the children of a node that
|
|
102
123
|
# has already been deleted, since that seems to trigger a memory leak in
|
|
103
124
|
# Nokogiri.
|
|
104
|
-
|
|
125
|
+
if (force_remove || !@elements.include?(name)) && !parent.nil?
|
|
105
126
|
# Elements like br, div, p, etc. need to be replaced with whitespace
|
|
106
127
|
# in order to preserve readability.
|
|
107
128
|
if @whitespace_elements.include?(name)
|
|
@@ -246,6 +267,55 @@ class Sanitize
|
|
|
246
267
|
node.unlink
|
|
247
268
|
end
|
|
248
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
|
|
249
319
|
end
|
|
250
320
|
end
|
|
251
321
|
end
|
data/lib/sanitize/version.rb
CHANGED
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
|
|
99
|
+
@transformers << Transformers::CleanComment
|
|
101
100
|
|
|
102
101
|
if @config[:elements].include?("style")
|
|
103
102
|
scss = Sanitize::CSS.new(config)
|