sanitize 6.1.2 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sanitize/css.rb CHANGED
@@ -1,325 +1,333 @@
1
- # encoding: utf-8
2
-
3
- require 'crass'
4
- require 'set'
5
-
6
- class Sanitize; class CSS
7
- attr_reader :config
8
-
9
- # -- Class Methods -----------------------------------------------------------
10
-
11
- # Sanitizes inline CSS style properties.
12
- #
13
- # This is most useful for sanitizing non-stylesheet fragments of CSS like you
14
- # would find in the `style` attribute of an HTML element. To sanitize a full
15
- # CSS stylesheet, use {.stylesheet}.
16
- #
17
- # @example
18
- # Sanitize::CSS.properties("background: url(foo.png); color: #fff;")
19
- #
20
- # @return [String] Sanitized CSS properties.
21
- def self.properties(css, config = {})
22
- self.new(config).properties(css)
23
- end
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
- # Sanitizes a full CSS stylesheet.
26
- #
27
- # A stylesheet may include selectors, at-rules, and comments. To sanitize only
28
- # inline style properties such as the contents of an HTML `style` attribute,
29
- # use {.properties}.
30
- #
31
- # @example
32
- # css = %[
33
- # .foo {
34
- # background: url(foo.png);
35
- # color: #fff;
36
- # }
37
- #
38
- # #bar {
39
- # font: 42pt 'Comic Sans MS';
40
- # }
41
- # ]
42
- #
43
- # Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED)
44
- #
45
- # @return [String] Sanitized CSS stylesheet.
46
- def self.stylesheet(css, config = {})
47
- self.new(config).stylesheet(css)
48
- end
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
- # Sanitizes the given Crass CSS parse tree and all its children, modifying it
51
- # in place.
52
- #
53
- # @example
54
- # css = %[
55
- # .foo {
56
- # background: url(foo.png);
57
- # color: #fff;
58
- # }
59
- #
60
- # #bar {
61
- # font: 42pt 'Comic Sans MS';
62
- # }
63
- # ]
64
- #
65
- # tree = Crass.parse(css)
66
- # Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED)
67
- #
68
- # @return [Array] Sanitized Crass CSS parse tree.
69
- def self.tree!(tree, config = {})
70
- self.new(config).tree!(tree)
71
- end
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
+ # @return [Array] Sanitized Crass CSS parse tree.
70
+ def self.tree!(tree, config = {})
71
+ new(config).tree!(tree)
72
+ end
72
73
 
73
- # -- Instance Methods --------------------------------------------------------
74
+ # -- Instance Methods ------------------------------------------------------
74
75
 
75
- # Returns a new Sanitize::CSS object initialized with the settings in
76
- # _config_.
77
- def initialize(config = {})
78
- @config = Config.merge(Config::DEFAULT[:css], config[:css] || config)
76
+ # Returns a new Sanitize::CSS object initialized with the settings in
77
+ # _config_.
78
+ def initialize(config = {})
79
+ @config = Config.merge(Config::DEFAULT[:css], config[:css] || config)
79
80
 
80
- @at_rules = Set.new(@config[:at_rules])
81
- @at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
82
- @at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
83
- @import_url_validator = @config[:import_url_validator]
84
- end
81
+ @at_rules = Set.new(@config[:at_rules])
82
+ @at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
83
+ @at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
84
+ @import_url_validator = @config[:import_url_validator]
85
+ end
85
86
 
86
- # Sanitizes inline CSS style properties.
87
- #
88
- # This is most useful for sanitizing non-stylesheet fragments of CSS like you
89
- # would find in the `style` attribute of an HTML element. To sanitize a full
90
- # CSS stylesheet, use {#stylesheet}.
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
87
+ # Sanitizes inline CSS style properties.
88
+ #
89
+ # This is most useful for sanitizing non-stylesheet fragments of CSS like
90
+ # you would find in the `style` attribute of an HTML element. To sanitize a
91
+ # full CSS stylesheet, use {#stylesheet}.
92
+ #
93
+ # @example
94
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
95
+ # scss.properties("background: url(foo.png); color: #fff;")
96
+ #
97
+ # @return [String] Sanitized CSS properties.
98
+ def properties(css)
99
+ tree = Crass.parse_properties(css,
100
+ preserve_comments: @config[:allow_comments],
101
+ preserve_hacks: @config[:allow_hacks])
102
+
103
+ tree!(tree)
104
+ Crass::Parser.stringify(tree)
105
+ end
105
106
 
106
- # Sanitizes a full CSS stylesheet.
107
- #
108
- # A stylesheet may include selectors, at-rules, and comments. To sanitize only
109
- # inline style properties such as the contents of an HTML `style` attribute,
110
- # use {#properties}.
111
- #
112
- # @example
113
- # css = %[
114
- # .foo {
115
- # background: url(foo.png);
116
- # color: #fff;
117
- # }
118
- #
119
- # #bar {
120
- # font: 42pt 'Comic Sans MS';
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
107
+ # Sanitizes a full CSS stylesheet.
108
+ #
109
+ # A stylesheet may include selectors, at-rules, and comments. To sanitize
110
+ # only inline style properties such as the contents of an HTML `style`
111
+ # attribute, use {#properties}.
112
+ #
113
+ # @example
114
+ # css = %[
115
+ # .foo {
116
+ # background: url(foo.png);
117
+ # color: #fff;
118
+ # }
119
+ #
120
+ # #bar {
121
+ # font: 42pt 'Comic Sans MS';
122
+ # }
123
+ # ]
124
+ #
125
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
126
+ # scss.stylesheet(css)
127
+ #
128
+ # @return [String] Sanitized CSS stylesheet.
129
+ def stylesheet(css)
130
+ tree = Crass.parse(css,
131
+ preserve_comments: @config[:allow_comments],
132
+ preserve_hacks: @config[:allow_hacks])
133
+
134
+ tree!(tree)
135
+ Crass::Parser.stringify(tree)
136
+ end
137
+
138
+ # Sanitizes the given Crass CSS parse tree and all its children, modifying
139
+ # it in place.
140
+ #
141
+ # @example
142
+ # css = %[
143
+ # .foo {
144
+ # background: url(foo.png);
145
+ # color: #fff;
146
+ # }
147
+ #
148
+ # #bar {
149
+ # font: 42pt 'Comic Sans MS';
150
+ # }
151
+ # ]
152
+ #
153
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
154
+ # tree = Crass.parse(css)
155
+ #
156
+ # scss.tree!(tree)
157
+ #
158
+ # @return [Array] Sanitized Crass CSS parse tree.
159
+ def tree!(tree)
160
+ preceded_by_property = false
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
136
186
 
137
- # Sanitizes the given Crass CSS parse tree and all its children, modifying it
138
- # in place.
139
- #
140
- # @example
141
- # css = %[
142
- # .foo {
143
- # background: url(foo.png);
144
- # color: #fff;
145
- # }
146
- #
147
- # #bar {
148
- # font: 42pt 'Comic Sans MS';
149
- # }
150
- # ]
151
- #
152
- # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
153
- # tree = Crass.parse(css)
154
- #
155
- # scss.tree!(tree)
156
- #
157
- # @return [Array] Sanitized Crass CSS parse tree.
158
- def tree!(tree)
159
- preceded_by_property = false
160
-
161
- tree.map! do |node|
162
- next nil if node.nil?
163
-
164
- case node[:node]
165
- when :at_rule
166
- preceded_by_property = false
167
- next at_rule!(node)
168
-
169
- when :comment
170
- next node if @config[:allow_comments]
171
-
172
- when :property
173
- prop = property!(node)
174
- preceded_by_property = !prop.nil?
175
- next prop
176
-
177
- when :semicolon
178
- # Only preserve the semicolon if it was preceded by an allowlisted
179
- # property. Otherwise, omit it in order to prevent redundant semicolons.
180
- if preceded_by_property
187
+ when :style_rule
181
188
  preceded_by_property = false
189
+ tree!(node[:children])
182
190
  next node
183
- end
184
191
 
185
- when :style_rule
186
- preceded_by_property = false
187
- tree!(node[:children])
188
- next node
192
+ when :whitespace
193
+ next node
194
+ end
189
195
 
190
- when :whitespace
191
- next node
196
+ nil
192
197
  end
193
198
 
194
- nil
199
+ tree
195
200
  end
196
201
 
197
- tree
198
- end
202
+ # -- Protected Instance Methods --------------------------------------------
203
+ protected
199
204
 
200
- # -- Protected Instance Methods ----------------------------------------------
201
- protected
205
+ # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
206
+ # current config doesn't allow this at-rule.
207
+ def at_rule!(rule)
208
+ name = rule[:name].downcase
202
209
 
203
- # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
204
- # current config doesn't allow this at-rule.
205
- def at_rule!(rule)
206
- name = rule[:name].downcase
210
+ if @at_rules_with_styles.include?(name)
211
+ styles = Crass::Parser.parse_rules(rule[:block],
212
+ preserve_comments: @config[:allow_comments],
213
+ preserve_hacks: @config[:allow_hacks])
207
214
 
208
- if @at_rules_with_styles.include?(name)
209
- styles = Crass::Parser.parse_rules(rule[:block],
210
- :preserve_comments => @config[:allow_comments],
211
- :preserve_hacks => @config[:allow_hacks])
215
+ rule[:block] = tree!(styles)
212
216
 
213
- rule[:block] = tree!(styles)
217
+ elsif @at_rules_with_properties.include?(name)
218
+ props = Crass::Parser.parse_properties(rule[:block],
219
+ preserve_comments: @config[:allow_comments],
220
+ preserve_hacks: @config[:allow_hacks])
214
221
 
215
- elsif @at_rules_with_properties.include?(name)
216
- props = Crass::Parser.parse_properties(rule[:block],
217
- :preserve_comments => @config[:allow_comments],
218
- :preserve_hacks => @config[:allow_hacks])
222
+ rule[:block] = tree!(props)
219
223
 
220
- rule[:block] = tree!(props)
224
+ elsif @at_rules.include?(name)
225
+ return nil if name == "import" && !import_url_allowed?(rule)
226
+ return nil if rule.has_key?(:block)
227
+ else
228
+ return nil
229
+ end
221
230
 
222
- elsif @at_rules.include?(name)
223
- return nil if name == "import" && !import_url_allowed?(rule)
224
- return nil if rule.has_key?(:block)
225
- else
226
- return nil
231
+ rule
227
232
  end
228
233
 
229
- rule
230
- end
234
+ # Returns `true` if the given CSS function name is an image-related function
235
+ # that may contain image URLs that need to be validated.
236
+ def image_function?(name)
237
+ ["image", "image-set", "-webkit-image-set"].include?(name)
238
+ end
231
239
 
232
- # Passes the URL value of an @import rule to a block to ensure
233
- # it's an allowed URL
234
- def import_url_allowed?(rule)
235
- return true unless @import_url_validator
240
+ # Passes the URL value of an @import rule to a block to ensure
241
+ # it's an allowed URL
242
+ def import_url_allowed?(rule)
243
+ return true unless @import_url_validator
236
244
 
237
- url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
245
+ url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
238
246
 
239
- # don't allow @imports with no URL value
240
- return false unless url_token && (import_url = url_token[:value])
247
+ # don't allow @imports with no URL value
248
+ return false unless url_token && (import_url = url_token[:value])
241
249
 
242
- @import_url_validator.call(import_url)
243
- end
250
+ @import_url_validator.call(import_url)
251
+ end
244
252
 
245
- # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
246
- # current config doesn't allow this property.
247
- def property!(prop)
248
- name = prop[:name].downcase
253
+ # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
254
+ # current config doesn't allow this property.
255
+ def property!(prop)
256
+ name = prop[:name].downcase
249
257
 
250
- # Preserve IE * and _ hacks if desired.
251
- if @config[:allow_hacks]
252
- name.slice!(0) if name =~ /\A[*_]/
253
- end
258
+ # Preserve IE * and _ hacks if desired.
259
+ if @config[:allow_hacks]
260
+ name.slice!(0) if /\A[*_]/.match?(name)
261
+ end
262
+
263
+ return nil unless @config[:properties].include?(name)
254
264
 
255
- return nil unless @config[:properties].include?(name)
265
+ nodes = prop[:children].dup
266
+ combined_value = +""
256
267
 
257
- nodes = prop[:children].dup
258
- combined_value = String.new
268
+ nodes.each do |child|
269
+ value = child[:value]
259
270
 
260
- nodes.each do |child|
261
- value = child[:value]
271
+ case child[:node]
272
+ when :ident
273
+ combined_value << value.downcase if String === value
262
274
 
263
- case child[:node]
264
- when :ident
265
- combined_value << value.downcase if String === value
275
+ when :function
276
+ if child.key?(:name)
277
+ name = child[:name].downcase
266
278
 
267
- when :function
268
- if child.key?(:name)
269
- name = child[:name].downcase
279
+ if name == "url"
280
+ return nil unless valid_url?(child)
281
+ end
270
282
 
271
- if name == 'url'
272
- return nil unless valid_url?(child)
283
+ if image_function?(name)
284
+ return nil unless valid_image?(child)
285
+ end
286
+
287
+ combined_value << name
288
+ return nil if name == "expression" || combined_value == "expression"
273
289
  end
274
290
 
275
- if name == 'image-set' || name == 'image'
276
- return nil unless valid_image?(child)
291
+ if Array === value
292
+ nodes.concat(value)
293
+ elsif String === value
294
+ lowercase_value = value.downcase
295
+ combined_value << lowercase_value
296
+ return nil if lowercase_value == "expression" || combined_value == "expression"
277
297
  end
278
298
 
279
- combined_value << name
280
- return nil if name == 'expression' || combined_value == 'expression'
281
- end
299
+ when :url
300
+ return nil unless valid_url?(child)
282
301
 
283
- if Array === value
284
- nodes.concat(value)
285
- elsif String === value
286
- lowercase_value = value.downcase
287
- combined_value << lowercase_value
288
- return nil if lowercase_value == 'expression' || combined_value == 'expression'
302
+ when :bad_url
303
+ return nil
289
304
  end
290
-
291
- when :url
292
- return nil unless valid_url?(child)
293
-
294
- when :bad_url
295
- return nil
296
305
  end
297
- end
298
306
 
299
- prop
300
- end
307
+ prop
308
+ end
301
309
 
302
- # Returns `true` if the given node (which may be of type `:url` or
303
- # `:function`, since the CSS syntax can produce both) uses an allowlisted
304
- # protocol.
305
- def valid_url?(node)
306
- type = node[:node]
310
+ # Returns `true` if the given node (which may be of type `:url` or
311
+ # `:function`, since the CSS syntax can produce both) uses an allowlisted
312
+ # protocol.
313
+ def valid_url?(node)
314
+ type = node[:node]
307
315
 
308
- if type == :function
309
- return false unless node.key?(:name) && node[:name].downcase == 'url'
310
- return false unless Array === node[:value]
316
+ if type == :function
317
+ return false unless node.key?(:name) && node[:name].downcase == "url"
318
+ return false unless Array === node[:value]
311
319
 
312
- # A URL function's `:value` should be an array containing no more than one
313
- # `:string` node and any number of `:whitespace` nodes.
314
- #
315
- # If it contains more than one `:string` node, or if it contains any other
316
- # nodes except `:whitespace` nodes, it's not valid.
317
- url_string_node = nil
320
+ # A URL function's `:value` should be an array containing no more than
321
+ # one `:string` node and any number of `:whitespace` nodes.
322
+ #
323
+ # If it contains more than one `:string` node, or if it contains any
324
+ # other nodes except `:whitespace` nodes, it's not valid.
325
+ url_string_node = nil
318
326
 
319
- node[:value].each do |token|
320
- return false unless Hash === token
327
+ node[:value].each do |token|
328
+ return false unless Hash === token
321
329
 
322
- case token[:node]
330
+ case token[:node]
323
331
  when :string
324
332
  return false unless url_string_node.nil?
325
333
  url_string_node = token
@@ -329,47 +337,45 @@ class Sanitize; class CSS
329
337
 
330
338
  else
331
339
  return false
340
+ end
332
341
  end
333
- end
334
342
 
335
- return false if url_string_node.nil?
336
- url = url_string_node[:value]
337
- elsif type == :url
338
- url = node[:value]
339
- else
340
- return false
341
- end
343
+ return false if url_string_node.nil?
344
+ url = url_string_node[:value]
345
+ elsif type == :url
346
+ url = node[:value]
347
+ else
348
+ return false
349
+ end
342
350
 
343
- if url =~ Sanitize::REGEX_PROTOCOL
344
- return @config[:protocols].include?($1.downcase)
345
- else
346
- return @config[:protocols].include?(:relative)
351
+ if url =~ Sanitize::REGEX_PROTOCOL
352
+ @config[:protocols].include?($1.downcase)
353
+ else
354
+ @config[:protocols].include?(:relative)
355
+ end
347
356
  end
348
357
 
349
- false
350
- end
351
-
352
- # Returns `true` if the given node (which is an `image` or `image-set` function) contains only strings
353
- # using an allowlisted protocol.
354
- def valid_image?(node)
355
- return false unless node[:node] == :function
356
- return false unless node.key?(:name) && ['image', 'image-set'].include?(node[:name].downcase)
357
- return false unless Array === node[:value]
358
+ # Returns `true` if the given node is an image-related function and contains
359
+ # only strings that use an allowlisted protocol.
360
+ def valid_image?(node)
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]
358
364
 
359
- node[:value].each do |token|
365
+ node[:value].each do |token|
360
366
  return false unless Hash === token
361
367
 
362
368
  case token[:node]
363
- when :string
364
- if token[:value] =~ Sanitize::REGEX_PROTOCOL
365
- return false unless @config[:protocols].include?($1.downcase)
366
- else
367
- return false unless @config[:protocols].include?(:relative)
368
- end
369
+ when :string
370
+ if token[:value] =~ Sanitize::REGEX_PROTOCOL
371
+ return false unless @config[:protocols].include?($1.downcase)
369
372
  else
370
- next
373
+ return false unless @config[:protocols].include?(:relative)
374
+ end
375
+ else
376
+ next
371
377
  end
372
378
  end
379
+ end
373
380
  end
374
-
375
- end; end
381
+ end