sanitize 6.1.3 → 7.0.1
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/{HISTORY.md → CHANGELOG.md} +50 -14
- data/LICENSE +3 -1
- data/README.md +130 -239
- data/lib/sanitize/config/basic.rb +15 -15
- data/lib/sanitize/config/default.rb +45 -45
- data/lib/sanitize/config/relaxed.rb +136 -32
- data/lib/sanitize/config/restricted.rb +2 -2
- data/lib/sanitize/config.rb +12 -14
- data/lib/sanitize/css.rb +403 -308
- data/lib/sanitize/transformers/clean_cdata.rb +9 -9
- data/lib/sanitize/transformers/clean_comment.rb +9 -9
- data/lib/sanitize/transformers/clean_css.rb +59 -55
- data/lib/sanitize/transformers/clean_doctype.rb +15 -15
- data/lib/sanitize/transformers/clean_element.rb +219 -237
- data/lib/sanitize/version.rb +3 -1
- data/lib/sanitize.rb +38 -38
- metadata +15 -56
- data/test/common.rb +0 -3
- data/test/test_clean_comment.rb +0 -47
- data/test/test_clean_css.rb +0 -67
- data/test/test_clean_doctype.rb +0 -71
- data/test/test_clean_element.rb +0 -552
- data/test/test_config.rb +0 -65
- data/test/test_malicious_css.rb +0 -55
- data/test/test_malicious_html.rb +0 -339
- data/test/test_parser.rb +0 -75
- data/test/test_sanitize.rb +0 -197
- data/test/test_sanitize_css.rb +0 -433
- data/test/test_transformers.rb +0 -230
data/lib/sanitize/css.rb
CHANGED
|
@@ -1,331 +1,428 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
class Sanitize
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
self.
|
|
23
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "crass"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
class Sanitize
|
|
7
|
+
class CSS
|
|
8
|
+
attr_reader :config
|
|
9
|
+
|
|
10
|
+
# -- Class Methods ---------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
# Sanitizes inline CSS style properties.
|
|
13
|
+
#
|
|
14
|
+
# This is most useful for sanitizing non-stylesheet fragments of CSS like
|
|
15
|
+
# you would find in the `style` attribute of an HTML element. To sanitize a
|
|
16
|
+
# full CSS stylesheet, use {.stylesheet}.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Sanitize::CSS.properties("background: url(foo.png); color: #fff;")
|
|
20
|
+
#
|
|
21
|
+
# @return [String] Sanitized CSS properties.
|
|
22
|
+
def self.properties(css, config = {})
|
|
23
|
+
new(config).properties(css)
|
|
24
|
+
end
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
# Sanitizes a full CSS stylesheet.
|
|
27
|
+
#
|
|
28
|
+
# A stylesheet may include selectors, at-rules, and comments. To sanitize
|
|
29
|
+
# only inline style properties such as the contents of an HTML `style`
|
|
30
|
+
# attribute, use {.properties}.
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# css = %[
|
|
34
|
+
# .foo {
|
|
35
|
+
# background: url(foo.png);
|
|
36
|
+
# color: #fff;
|
|
37
|
+
# }
|
|
38
|
+
#
|
|
39
|
+
# #bar {
|
|
40
|
+
# font: 42pt 'Comic Sans MS';
|
|
41
|
+
# }
|
|
42
|
+
# ]
|
|
43
|
+
#
|
|
44
|
+
# Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED)
|
|
45
|
+
#
|
|
46
|
+
# @return [String] Sanitized CSS stylesheet.
|
|
47
|
+
def self.stylesheet(css, config = {})
|
|
48
|
+
new(config).stylesheet(css)
|
|
49
|
+
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
51
|
+
# Sanitizes the given Crass CSS parse tree and all its children, modifying
|
|
52
|
+
# it in place.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# css = %[
|
|
56
|
+
# .foo {
|
|
57
|
+
# background: url(foo.png);
|
|
58
|
+
# color: #fff;
|
|
59
|
+
# }
|
|
60
|
+
#
|
|
61
|
+
# #bar {
|
|
62
|
+
# font: 42pt 'Comic Sans MS';
|
|
63
|
+
# }
|
|
64
|
+
# ]
|
|
65
|
+
#
|
|
66
|
+
# tree = Crass.parse(css)
|
|
67
|
+
# Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED)
|
|
68
|
+
#
|
|
69
|
+
# The tree must be a list of rules, such as the tree returned by
|
|
70
|
+
# `Crass.parse`. To sanitize a list of declarations, such as the contents
|
|
71
|
+
# of an HTML `style` attribute, use {.properties}.
|
|
72
|
+
#
|
|
73
|
+
# @return [Array] Sanitized Crass CSS parse tree.
|
|
74
|
+
def self.tree!(tree, config = {})
|
|
75
|
+
new(config).tree!(tree)
|
|
76
|
+
end
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
# -- Instance Methods ------------------------------------------------------
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
# Returns a new Sanitize::CSS object initialized with the settings in
|
|
81
|
+
# _config_.
|
|
82
|
+
def initialize(config = {})
|
|
83
|
+
@config = Config.merge(Config::DEFAULT[:css], config[:css] || config)
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end
|
|
85
|
+
@at_rules = Set.new(@config[:at_rules])
|
|
86
|
+
@at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
|
|
87
|
+
@at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
|
|
88
|
+
@import_url_validator = @config[:import_url_validator]
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
# @example
|
|
93
|
-
# scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
|
|
94
|
-
# scss.properties("background: url(foo.png); color: #fff;")
|
|
95
|
-
#
|
|
96
|
-
# @return [String] Sanitized CSS properties.
|
|
97
|
-
def properties(css)
|
|
98
|
-
tree = Crass.parse_properties(css,
|
|
99
|
-
:preserve_comments => @config[:allow_comments],
|
|
100
|
-
:preserve_hacks => @config[:allow_hacks])
|
|
101
|
-
|
|
102
|
-
tree!(tree)
|
|
103
|
-
Crass::Parser.stringify(tree)
|
|
104
|
-
end
|
|
90
|
+
@parse_options = {
|
|
91
|
+
preserve_comments: @config[:allow_comments],
|
|
92
|
+
preserve_hacks: @config[:allow_hacks]
|
|
93
|
+
}.freeze
|
|
94
|
+
end
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# ]
|
|
123
|
-
#
|
|
124
|
-
# scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
|
|
125
|
-
# scss.stylesheet(css)
|
|
126
|
-
#
|
|
127
|
-
# @return [String] Sanitized CSS stylesheet.
|
|
128
|
-
def stylesheet(css)
|
|
129
|
-
tree = Crass.parse(css,
|
|
130
|
-
:preserve_comments => @config[:allow_comments],
|
|
131
|
-
:preserve_hacks => @config[:allow_hacks])
|
|
132
|
-
|
|
133
|
-
tree!(tree)
|
|
134
|
-
Crass::Parser.stringify(tree)
|
|
135
|
-
end
|
|
96
|
+
# Sanitizes inline CSS style properties.
|
|
97
|
+
#
|
|
98
|
+
# This is most useful for sanitizing non-stylesheet fragments of CSS like
|
|
99
|
+
# you would find in the `style` attribute of an HTML element. To sanitize a
|
|
100
|
+
# full CSS stylesheet, use {#stylesheet}.
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
|
|
104
|
+
# scss.properties("background: url(foo.png); color: #fff;")
|
|
105
|
+
#
|
|
106
|
+
# @return [String] Sanitized CSS properties.
|
|
107
|
+
def properties(css)
|
|
108
|
+
tree = Crass.parse_properties(css, @parse_options)
|
|
109
|
+
sanitize_tree!(tree, declarations: true)
|
|
110
|
+
Crass::Parser.stringify(tree)
|
|
111
|
+
end
|
|
136
112
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
113
|
+
# Sanitizes a full CSS stylesheet.
|
|
114
|
+
#
|
|
115
|
+
# A stylesheet may include selectors, at-rules, and comments. To sanitize
|
|
116
|
+
# only inline style properties such as the contents of an HTML `style`
|
|
117
|
+
# attribute, use {#properties}.
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# css = %[
|
|
121
|
+
# .foo {
|
|
122
|
+
# background: url(foo.png);
|
|
123
|
+
# color: #fff;
|
|
124
|
+
# }
|
|
125
|
+
#
|
|
126
|
+
# #bar {
|
|
127
|
+
# font: 42pt 'Comic Sans MS';
|
|
128
|
+
# }
|
|
129
|
+
# ]
|
|
130
|
+
#
|
|
131
|
+
# scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
|
|
132
|
+
# scss.stylesheet(css)
|
|
133
|
+
#
|
|
134
|
+
# @return [String] Sanitized CSS stylesheet.
|
|
135
|
+
def stylesheet(css)
|
|
136
|
+
tree = Crass.parse(css, @parse_options)
|
|
137
|
+
tree!(tree)
|
|
138
|
+
Crass::Parser.stringify(tree)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Sanitizes the given Crass CSS parse tree and all its children, modifying
|
|
142
|
+
# it in place.
|
|
143
|
+
#
|
|
144
|
+
# @example
|
|
145
|
+
# css = %[
|
|
146
|
+
# .foo {
|
|
147
|
+
# background: url(foo.png);
|
|
148
|
+
# color: #fff;
|
|
149
|
+
# }
|
|
150
|
+
#
|
|
151
|
+
# #bar {
|
|
152
|
+
# font: 42pt 'Comic Sans MS';
|
|
153
|
+
# }
|
|
154
|
+
# ]
|
|
155
|
+
#
|
|
156
|
+
# scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
|
|
157
|
+
# tree = Crass.parse(css)
|
|
158
|
+
#
|
|
159
|
+
# scss.tree!(tree)
|
|
160
|
+
#
|
|
161
|
+
# The tree must be a list of rules, such as the tree returned by
|
|
162
|
+
# `Crass.parse`. To sanitize a list of declarations, such as the contents
|
|
163
|
+
# of an HTML `style` attribute, use {#properties}.
|
|
164
|
+
#
|
|
165
|
+
# @return [Array] Sanitized Crass CSS parse tree.
|
|
166
|
+
def tree!(tree)
|
|
167
|
+
sanitize_tree!(tree, declarations: false)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# -- Protected Instance Methods --------------------------------------------
|
|
171
|
+
protected
|
|
172
|
+
|
|
173
|
+
# Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
|
|
174
|
+
# current config doesn't allow this at-rule.
|
|
175
|
+
#
|
|
176
|
+
# If _declarations_ is `true`, the at-rule appears in a list of
|
|
177
|
+
# declarations (such as the contents of a style rule) rather than in a list
|
|
178
|
+
# of rules.
|
|
179
|
+
def at_rule!(rule, declarations:)
|
|
180
|
+
name = rule[:name].downcase
|
|
181
|
+
|
|
182
|
+
return nil if contains_invalid_node?(rule[:prelude])
|
|
183
|
+
|
|
184
|
+
if @at_rules_with_styles.include?(name)
|
|
185
|
+
# Browsers that support CSS nesting parse the block of an at-rule like
|
|
186
|
+
# `@media` that appears in a list of declarations as more declarations
|
|
187
|
+
# rather than as rules, so it must be sanitized as declarations.
|
|
188
|
+
# Otherwise a declaration like `position:fixed;a{}` would be parsed as
|
|
189
|
+
# a style rule with the selector `position:fixed;a` and would bypass
|
|
190
|
+
# the property allowlist.
|
|
191
|
+
styles = if declarations
|
|
192
|
+
Crass::Parser.parse_properties(rule[:block], @parse_options)
|
|
193
|
+
else
|
|
194
|
+
Crass::Parser.parse_rules(rule[:block], @parse_options)
|
|
183
195
|
end
|
|
184
196
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
rule[:block] = sanitize_tree!(styles, declarations: declarations)
|
|
198
|
+
|
|
199
|
+
elsif @at_rules_with_properties.include?(name)
|
|
200
|
+
props = Crass::Parser.parse_properties(rule[:block], @parse_options)
|
|
201
|
+
rule[:block] = sanitize_tree!(props, declarations: true)
|
|
189
202
|
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
elsif @at_rules.include?(name)
|
|
204
|
+
return nil if name == "import" && !import_url_allowed?(rule)
|
|
205
|
+
return nil if rule.has_key?(:block)
|
|
206
|
+
else
|
|
207
|
+
return nil
|
|
192
208
|
end
|
|
193
209
|
|
|
194
|
-
|
|
210
|
+
rule
|
|
195
211
|
end
|
|
196
212
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
213
|
+
# Returns `true` if the given array of Crass nodes contains an invalid node
|
|
214
|
+
# (see {#invalid_node?}) at any depth.
|
|
215
|
+
def contains_invalid_node?(nodes)
|
|
216
|
+
contains_node?(nodes) { |node| invalid_node?(node) }
|
|
217
|
+
end
|
|
202
218
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
# Returns `true` if the given array of Crass nodes contains a node at any
|
|
220
|
+
# depth for which the given block returns a truthy value.
|
|
221
|
+
def contains_node?(nodes)
|
|
222
|
+
return false unless Array === nodes
|
|
207
223
|
|
|
208
|
-
|
|
209
|
-
styles = Crass::Parser.parse_rules(rule[:block],
|
|
210
|
-
:preserve_comments => @config[:allow_comments],
|
|
211
|
-
:preserve_hacks => @config[:allow_hacks])
|
|
224
|
+
stack = nodes.dup
|
|
212
225
|
|
|
213
|
-
|
|
226
|
+
until stack.empty?
|
|
227
|
+
node = stack.pop
|
|
228
|
+
next unless Hash === node
|
|
229
|
+
return true if yield(node)
|
|
214
230
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
:preserve_hacks => @config[:allow_hacks])
|
|
231
|
+
value = node[:value]
|
|
232
|
+
stack.concat(value) if Array === value
|
|
233
|
+
end
|
|
219
234
|
|
|
220
|
-
|
|
235
|
+
false
|
|
236
|
+
end
|
|
221
237
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return nil
|
|
238
|
+
# Returns `true` if the given CSS function name is an image-related function
|
|
239
|
+
# that may contain image URLs that need to be validated.
|
|
240
|
+
def image_function?(name)
|
|
241
|
+
["image", "image-set", "-webkit-image-set"].include?(name)
|
|
227
242
|
end
|
|
228
243
|
|
|
229
|
-
rule
|
|
230
|
-
|
|
244
|
+
# Passes the URL value of an @import rule to a block to ensure
|
|
245
|
+
# it's an allowed URL
|
|
246
|
+
def import_url_allowed?(rule)
|
|
247
|
+
return true unless @import_url_validator
|
|
231
248
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
end
|
|
249
|
+
url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
|
|
250
|
+
|
|
251
|
+
# don't allow @imports with no URL value
|
|
252
|
+
return false unless url_token && (import_url = url_token[:value])
|
|
237
253
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def import_url_allowed?(rule)
|
|
241
|
-
return true unless @import_url_validator
|
|
254
|
+
@import_url_validator.call(import_url)
|
|
255
|
+
end
|
|
242
256
|
|
|
243
|
-
|
|
257
|
+
# Returns `true` if the given Crass node is a `:bad_url` or `:error` node.
|
|
258
|
+
#
|
|
259
|
+
# A bad URL is invalid CSS that a browser will discard, but a parser that
|
|
260
|
+
# disagrees with the browser about where a bad URL ends could be tricked
|
|
261
|
+
# into hiding CSS inside it.
|
|
262
|
+
#
|
|
263
|
+
# An `:error` node is CSS that Crass couldn't or wouldn't parse, such as a
|
|
264
|
+
# block nested more deeply than Crass's maximum nesting depth. Its contents
|
|
265
|
+
# are never inspected, but they're preserved verbatim when the tree is
|
|
266
|
+
# stringified, so a browser would still see them.
|
|
267
|
+
#
|
|
268
|
+
# It's safest to reject any construct that contains either one.
|
|
269
|
+
def invalid_node?(node)
|
|
270
|
+
node[:node] == :bad_url || node[:node] == :error
|
|
271
|
+
end
|
|
244
272
|
|
|
245
|
-
#
|
|
246
|
-
|
|
273
|
+
# Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
|
|
274
|
+
# current config doesn't allow this property.
|
|
275
|
+
def property!(prop)
|
|
276
|
+
name = prop[:name].downcase
|
|
247
277
|
|
|
248
|
-
|
|
249
|
-
|
|
278
|
+
# Preserve IE * and _ hacks if desired.
|
|
279
|
+
if @config[:allow_hacks]
|
|
280
|
+
name.slice!(0) if /\A[*_]/.match?(name)
|
|
281
|
+
end
|
|
250
282
|
|
|
251
|
-
|
|
252
|
-
# current config doesn't allow this property.
|
|
253
|
-
def property!(prop)
|
|
254
|
-
name = prop[:name].downcase
|
|
283
|
+
return nil unless @config[:properties].include?(name)
|
|
255
284
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
name.slice!(0) if name =~ /\A[*_]/
|
|
259
|
-
end
|
|
285
|
+
nodes = prop[:children].dup
|
|
286
|
+
combined_value = +""
|
|
260
287
|
|
|
261
|
-
|
|
288
|
+
nodes.each do |child|
|
|
289
|
+
# Reject properties that contain an invalid node at any depth.
|
|
290
|
+
return nil if invalid_node?(child)
|
|
262
291
|
|
|
263
|
-
|
|
264
|
-
combined_value = String.new
|
|
292
|
+
value = child[:value]
|
|
265
293
|
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
case child[:node]
|
|
295
|
+
when :ident
|
|
296
|
+
combined_value << value.downcase if String === value
|
|
268
297
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
298
|
+
when :function
|
|
299
|
+
if child.key?(:name)
|
|
300
|
+
name = child[:name].downcase
|
|
272
301
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
302
|
+
if name == "url"
|
|
303
|
+
return nil unless valid_url?(child)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
if image_function?(name)
|
|
307
|
+
return nil unless valid_image?(child)
|
|
308
|
+
end
|
|
276
309
|
|
|
277
|
-
|
|
278
|
-
return nil
|
|
310
|
+
combined_value << name
|
|
311
|
+
return nil if name == "expression" || combined_value == "expression"
|
|
279
312
|
end
|
|
280
313
|
|
|
281
|
-
if
|
|
282
|
-
|
|
314
|
+
if Array === value
|
|
315
|
+
nodes.concat(value)
|
|
316
|
+
elsif String === value
|
|
317
|
+
lowercase_value = value.downcase
|
|
318
|
+
combined_value << lowercase_value
|
|
319
|
+
return nil if lowercase_value == "expression" || combined_value == "expression"
|
|
283
320
|
end
|
|
284
321
|
|
|
285
|
-
|
|
286
|
-
return nil
|
|
322
|
+
when :url
|
|
323
|
+
return nil unless valid_url?(child)
|
|
324
|
+
|
|
325
|
+
when :simple_block
|
|
326
|
+
# Reject properties that contain a `{}` block at any depth.
|
|
327
|
+
#
|
|
328
|
+
# A `{}` block isn't valid in a standard property value, and browsers
|
|
329
|
+
# that support CSS nesting will reparse an invalid declaration that
|
|
330
|
+
# contains one as a nested style rule (e.g.
|
|
331
|
+
# `color:hover{position:fixed}` becomes the rule
|
|
332
|
+
# `color:hover { position: fixed }`), which would allow disallowed
|
|
333
|
+
# properties to be smuggled past the sanitizer.
|
|
334
|
+
return nil if child[:start] == "{"
|
|
335
|
+
|
|
336
|
+
nodes.concat(value) if Array === value
|
|
287
337
|
end
|
|
338
|
+
end
|
|
288
339
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
elsif String === value
|
|
292
|
-
lowercase_value = value.downcase
|
|
293
|
-
combined_value << lowercase_value
|
|
294
|
-
return nil if lowercase_value == 'expression' || combined_value == 'expression'
|
|
295
|
-
end
|
|
340
|
+
prop
|
|
341
|
+
end
|
|
296
342
|
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
# Sanitizes the given Crass CSS parse tree and all its children, modifying
|
|
344
|
+
# it in place. See {#tree!}.
|
|
345
|
+
#
|
|
346
|
+
# If _declarations_ is `true`, the tree is a list of declarations (such as
|
|
347
|
+
# the contents of a style rule) rather than a list of rules.
|
|
348
|
+
def sanitize_tree!(tree, declarations:)
|
|
349
|
+
preceded_by_property = false
|
|
299
350
|
|
|
300
|
-
|
|
301
|
-
|
|
351
|
+
tree.map! do |node|
|
|
352
|
+
next nil if node.nil?
|
|
353
|
+
|
|
354
|
+
case node[:node]
|
|
355
|
+
when :at_rule
|
|
356
|
+
preceded_by_property = false
|
|
357
|
+
next at_rule!(node, declarations: declarations)
|
|
358
|
+
|
|
359
|
+
when :comment
|
|
360
|
+
next node if @config[:allow_comments]
|
|
361
|
+
|
|
362
|
+
when :property
|
|
363
|
+
prop = property!(node)
|
|
364
|
+
preceded_by_property = !prop.nil?
|
|
365
|
+
next prop
|
|
366
|
+
|
|
367
|
+
when :semicolon
|
|
368
|
+
# Only preserve the semicolon if it was preceded by an allowlisted
|
|
369
|
+
# property. Otherwise, omit it in order to prevent redundant
|
|
370
|
+
# semicolons.
|
|
371
|
+
if preceded_by_property
|
|
372
|
+
preceded_by_property = false
|
|
373
|
+
next node
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
when :style_rule
|
|
377
|
+
preceded_by_property = false
|
|
378
|
+
|
|
379
|
+
# Reject style rules whose selectors contain an invalid node (see
|
|
380
|
+
# {#invalid_node?}) or a semicolon at any depth.
|
|
381
|
+
#
|
|
382
|
+
# A semicolon is never valid in a selector, but if a style rule
|
|
383
|
+
# appears where a browser expects declarations (such as the contents
|
|
384
|
+
# of an `@media` rule in a list of declarations), the browser will
|
|
385
|
+
# treat everything before the semicolon as a declaration. For
|
|
386
|
+
# example, `position:fixed;a{}` would be treated as the declaration
|
|
387
|
+
# `position:fixed` followed by the style rule `a{}`.
|
|
388
|
+
next nil if contains_node?(node.dig(:selector, :tokens)) { |token|
|
|
389
|
+
invalid_node?(token) || token[:node] == :semicolon
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
sanitize_tree!(node[:children], declarations: true)
|
|
393
|
+
next node
|
|
394
|
+
|
|
395
|
+
when :whitespace
|
|
396
|
+
next node
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
nil
|
|
302
400
|
end
|
|
303
|
-
end
|
|
304
401
|
|
|
305
|
-
|
|
306
|
-
|
|
402
|
+
tree
|
|
403
|
+
end
|
|
307
404
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
405
|
+
# Returns `true` if the given node (which may be of type `:url` or
|
|
406
|
+
# `:function`, since the CSS syntax can produce both) uses an allowlisted
|
|
407
|
+
# protocol.
|
|
408
|
+
def valid_url?(node)
|
|
409
|
+
type = node[:node]
|
|
313
410
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
411
|
+
if type == :function
|
|
412
|
+
return false unless node.key?(:name) && node[:name].downcase == "url"
|
|
413
|
+
return false unless Array === node[:value]
|
|
317
414
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
415
|
+
# A URL function's `:value` should be an array containing no more than
|
|
416
|
+
# one `:string` node and any number of `:whitespace` nodes.
|
|
417
|
+
#
|
|
418
|
+
# If it contains more than one `:string` node, or if it contains any
|
|
419
|
+
# other nodes except `:whitespace` nodes, it's not valid.
|
|
420
|
+
url_string_node = nil
|
|
324
421
|
|
|
325
|
-
|
|
326
|
-
|
|
422
|
+
node[:value].each do |token|
|
|
423
|
+
return false unless Hash === token
|
|
327
424
|
|
|
328
|
-
|
|
425
|
+
case token[:node]
|
|
329
426
|
when :string
|
|
330
427
|
return false unless url_string_node.nil?
|
|
331
428
|
url_string_node = token
|
|
@@ -335,47 +432,45 @@ class Sanitize; class CSS
|
|
|
335
432
|
|
|
336
433
|
else
|
|
337
434
|
return false
|
|
435
|
+
end
|
|
338
436
|
end
|
|
339
|
-
end
|
|
340
437
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
438
|
+
return false if url_string_node.nil?
|
|
439
|
+
url = url_string_node[:value]
|
|
440
|
+
elsif type == :url
|
|
441
|
+
url = node[:value]
|
|
442
|
+
else
|
|
443
|
+
return false
|
|
444
|
+
end
|
|
348
445
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
446
|
+
if url =~ Sanitize::REGEX_PROTOCOL
|
|
447
|
+
@config[:protocols].include?($1.downcase)
|
|
448
|
+
else
|
|
449
|
+
@config[:protocols].include?(:relative)
|
|
450
|
+
end
|
|
353
451
|
end
|
|
354
452
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return false unless node[:node] == :function
|
|
362
|
-
return false unless node.key?(:name) && image_function?(node[:name].downcase)
|
|
363
|
-
return false unless Array === node[:value]
|
|
453
|
+
# Returns `true` if the given node is an image-related function and contains
|
|
454
|
+
# only strings that use an allowlisted protocol.
|
|
455
|
+
def valid_image?(node)
|
|
456
|
+
return false unless node[:node] == :function
|
|
457
|
+
return false unless node.key?(:name) && image_function?(node[:name].downcase)
|
|
458
|
+
return false unless Array === node[:value]
|
|
364
459
|
|
|
365
|
-
|
|
460
|
+
node[:value].each do |token|
|
|
366
461
|
return false unless Hash === token
|
|
367
462
|
|
|
368
463
|
case token[:node]
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
else
|
|
373
|
-
return false unless @config[:protocols].include?(:relative)
|
|
374
|
-
end
|
|
464
|
+
when :string
|
|
465
|
+
if token[:value] =~ Sanitize::REGEX_PROTOCOL
|
|
466
|
+
return false unless @config[:protocols].include?($1.downcase)
|
|
375
467
|
else
|
|
376
|
-
|
|
468
|
+
return false unless @config[:protocols].include?(:relative)
|
|
469
|
+
end
|
|
470
|
+
else
|
|
471
|
+
next
|
|
377
472
|
end
|
|
378
473
|
end
|
|
474
|
+
end
|
|
379
475
|
end
|
|
380
|
-
|
|
381
|
-
end; end
|
|
476
|
+
end
|