sanitize 2.1.1 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sanitize might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/HISTORY.md +520 -55
- data/LICENSE +1 -1
- data/README.md +438 -168
- data/lib/sanitize/config/basic.rb +12 -32
- data/lib/sanitize/config/default.rb +118 -0
- data/lib/sanitize/config/relaxed.rb +716 -53
- data/lib/sanitize/config/restricted.rb +3 -23
- data/lib/sanitize/config.rb +53 -79
- data/lib/sanitize/css.rb +348 -0
- data/lib/sanitize/transformers/clean_cdata.rb +3 -3
- data/lib/sanitize/transformers/clean_comment.rb +6 -3
- data/lib/sanitize/transformers/clean_css.rb +57 -0
- data/lib/sanitize/transformers/clean_doctype.rb +19 -0
- data/lib/sanitize/transformers/clean_element.rb +192 -124
- data/lib/sanitize/version.rb +3 -1
- data/lib/sanitize.rb +172 -143
- data/test/common.rb +3 -0
- data/test/test_clean_comment.rb +47 -0
- data/test/test_clean_css.rb +67 -0
- data/test/test_clean_doctype.rb +71 -0
- data/test/test_clean_element.rb +545 -0
- data/test/test_config.rb +65 -0
- data/test/test_malicious_css.rb +42 -0
- data/test/test_malicious_html.rb +235 -0
- data/test/test_parser.rb +75 -0
- data/test/test_sanitize.rb +151 -675
- data/test/test_sanitize_css.rb +424 -0
- data/test/test_transformers.rb +230 -0
- metadata +44 -41
@@ -0,0 +1,424 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative 'common'
|
3
|
+
|
4
|
+
describe 'Sanitize::CSS' do
|
5
|
+
make_my_diffs_pretty!
|
6
|
+
parallelize_me!
|
7
|
+
|
8
|
+
describe 'instance methods' do
|
9
|
+
before do
|
10
|
+
@default = Sanitize::CSS.new
|
11
|
+
@relaxed = Sanitize::CSS.new(Sanitize::Config::RELAXED[:css])
|
12
|
+
@custom = Sanitize::CSS.new(:properties => %w[background color width])
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#properties' do
|
16
|
+
it 'should sanitize CSS properties' do
|
17
|
+
css = 'background: #fff; width: expression(alert("hi"));'
|
18
|
+
|
19
|
+
@default.properties(css).must_equal ' '
|
20
|
+
@relaxed.properties(css).must_equal 'background: #fff; '
|
21
|
+
@custom.properties(css).must_equal 'background: #fff; '
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should allow allowlisted URL protocols' do
|
25
|
+
[
|
26
|
+
"background: url(relative.jpg)",
|
27
|
+
"background: url('relative.jpg')",
|
28
|
+
"background: url(http://example.com/http.jpg)",
|
29
|
+
"background: url('ht\\tp://example.com/http.jpg')",
|
30
|
+
"background: url(https://example.com/https.jpg)",
|
31
|
+
"background: url('https://example.com/https.jpg')",
|
32
|
+
].each do |css|
|
33
|
+
@default.properties(css).must_equal ''
|
34
|
+
@relaxed.properties(css).must_equal css
|
35
|
+
@custom.properties(css).must_equal ''
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should not allow non-allowlisted URL protocols' do
|
40
|
+
[
|
41
|
+
"background: url(javascript:alert(0))",
|
42
|
+
"background: url(ja\\56 ascript:alert(0))",
|
43
|
+
"background: url('javascript:foo')",
|
44
|
+
"background: url('ja\\56 ascript:alert(0)')",
|
45
|
+
"background: url('ja\\va\\script\\:alert(0)')",
|
46
|
+
"background: url('javas\\\ncript:alert(0)')",
|
47
|
+
"background: url('java\\0script:foo')"
|
48
|
+
].each do |css|
|
49
|
+
@default.properties(css).must_equal ''
|
50
|
+
@relaxed.properties(css).must_equal ''
|
51
|
+
@custom.properties(css).must_equal ''
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should not allow -moz-binding' do
|
56
|
+
css = "-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss')"
|
57
|
+
|
58
|
+
@default.properties(css).must_equal ''
|
59
|
+
@relaxed.properties(css).must_equal ''
|
60
|
+
@custom.properties(css).must_equal ''
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should not allow expressions' do
|
64
|
+
[
|
65
|
+
"width:expression(alert(1))",
|
66
|
+
"width: /**/expression(alert(1)",
|
67
|
+
"width:e\\78 pression(\n\nalert(\n1)",
|
68
|
+
"width:\nexpression(alert(1));",
|
69
|
+
"xss:expression(alert(1))",
|
70
|
+
"height: foo(expression(alert(1)));"
|
71
|
+
].each do |css|
|
72
|
+
@default.properties(css).must_equal ''
|
73
|
+
@relaxed.properties(css).must_equal ''
|
74
|
+
@custom.properties(css).must_equal ''
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should not allow behaviors' do
|
79
|
+
css = "behavior: url(xss.htc);"
|
80
|
+
|
81
|
+
@default.properties(css).must_equal ''
|
82
|
+
@relaxed.properties(css).must_equal ''
|
83
|
+
@custom.properties(css).must_equal ''
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'when :allow_comments is true' do
|
87
|
+
it 'should preserve comments' do
|
88
|
+
@relaxed.properties('color: #fff; /* comment */ width: 100px;')
|
89
|
+
.must_equal 'color: #fff; /* comment */ width: 100px;'
|
90
|
+
|
91
|
+
@relaxed.properties("color: #fff; /* \n\ncomment */ width: 100px;")
|
92
|
+
.must_equal "color: #fff; /* \n\ncomment */ width: 100px;"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'when :allow_comments is false' do
|
97
|
+
it 'should strip comments' do
|
98
|
+
@custom.properties('color: #fff; /* comment */ width: 100px;')
|
99
|
+
.must_equal 'color: #fff; width: 100px;'
|
100
|
+
|
101
|
+
@custom.properties("color: #fff; /* \n\ncomment */ width: 100px;")
|
102
|
+
.must_equal 'color: #fff; width: 100px;'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'when :allow_hacks is true' do
|
107
|
+
it 'should allow common CSS hacks' do
|
108
|
+
@relaxed.properties('_border: 1px solid #fff; *width: 10px')
|
109
|
+
.must_equal '_border: 1px solid #fff; *width: 10px'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'when :allow_hacks is false' do
|
114
|
+
it 'should not allow common CSS hacks' do
|
115
|
+
@custom.properties('_border: 1px solid #fff; *width: 10px')
|
116
|
+
.must_equal ' '
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#stylesheet' do
|
122
|
+
it 'should sanitize a CSS stylesheet' do
|
123
|
+
css = %[
|
124
|
+
/* Yay CSS! */
|
125
|
+
.foo { color: #fff; }
|
126
|
+
#bar { background: url(yay.jpg); }
|
127
|
+
|
128
|
+
@media screen (max-width:480px) {
|
129
|
+
.foo { width: 400px; }
|
130
|
+
#bar:not(.baz) { height: 100px; }
|
131
|
+
}
|
132
|
+
].strip
|
133
|
+
|
134
|
+
@default.stylesheet(css).strip.must_equal %[
|
135
|
+
.foo { }
|
136
|
+
#bar { }
|
137
|
+
].strip
|
138
|
+
|
139
|
+
@relaxed.stylesheet(css).must_equal css
|
140
|
+
|
141
|
+
@custom.stylesheet(css).strip.must_equal %[
|
142
|
+
.foo { color: #fff; }
|
143
|
+
#bar { }
|
144
|
+
].strip
|
145
|
+
end
|
146
|
+
|
147
|
+
describe 'when :allow_comments is true' do
|
148
|
+
it 'should preserve comments' do
|
149
|
+
@relaxed.stylesheet('.foo { color: #fff; /* comment */ width: 100px; }')
|
150
|
+
.must_equal '.foo { color: #fff; /* comment */ width: 100px; }'
|
151
|
+
|
152
|
+
@relaxed.stylesheet(".foo { color: #fff; /* \n\ncomment */ width: 100px; }")
|
153
|
+
.must_equal ".foo { color: #fff; /* \n\ncomment */ width: 100px; }"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe 'when :allow_comments is false' do
|
158
|
+
it 'should strip comments' do
|
159
|
+
@custom.stylesheet('.foo { color: #fff; /* comment */ width: 100px; }')
|
160
|
+
.must_equal '.foo { color: #fff; width: 100px; }'
|
161
|
+
|
162
|
+
@custom.stylesheet(".foo { color: #fff; /* \n\ncomment */ width: 100px; }")
|
163
|
+
.must_equal '.foo { color: #fff; width: 100px; }'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe 'when :allow_hacks is true' do
|
168
|
+
it 'should allow common CSS hacks' do
|
169
|
+
@relaxed.stylesheet('.foo { _border: 1px solid #fff; *width: 10px }')
|
170
|
+
.must_equal '.foo { _border: 1px solid #fff; *width: 10px }'
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe 'when :allow_hacks is false' do
|
175
|
+
it 'should not allow common CSS hacks' do
|
176
|
+
@custom.stylesheet('.foo { _border: 1px solid #fff; *width: 10px }')
|
177
|
+
.must_equal '.foo { }'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe '#tree!' do
|
183
|
+
it 'should sanitize a Crass CSS parse tree' do
|
184
|
+
tree = Crass.parse(String.new("@import url(foo.css);\n") <<
|
185
|
+
".foo { background: #fff; font: 16pt 'Comic Sans MS'; }\n" <<
|
186
|
+
"#bar { top: 125px; background: green; }")
|
187
|
+
|
188
|
+
@custom.tree!(tree).must_be_same_as tree
|
189
|
+
|
190
|
+
Crass::Parser.stringify(tree).must_equal String.new("\n") <<
|
191
|
+
".foo { background: #fff; }\n" <<
|
192
|
+
"#bar { background: green; }"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe 'class methods' do
|
198
|
+
describe '.properties' do
|
199
|
+
it 'should sanitize CSS properties with the given config' do
|
200
|
+
css = 'background: #fff; width: expression(alert("hi"));'
|
201
|
+
|
202
|
+
Sanitize::CSS.properties(css).must_equal ' '
|
203
|
+
Sanitize::CSS.properties(css, Sanitize::Config::RELAXED[:css]).must_equal 'background: #fff; '
|
204
|
+
Sanitize::CSS.properties(css, :properties => %w[background color width]).must_equal 'background: #fff; '
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe '.stylesheet' do
|
209
|
+
it 'should sanitize a CSS stylesheet with the given config' do
|
210
|
+
css = %[
|
211
|
+
/* Yay CSS! */
|
212
|
+
.foo { color: #fff; }
|
213
|
+
#bar { background: url(yay.jpg); }
|
214
|
+
|
215
|
+
@media screen (max-width:480px) {
|
216
|
+
.foo { width: 400px; }
|
217
|
+
#bar:not(.baz) { height: 100px; }
|
218
|
+
}
|
219
|
+
].strip
|
220
|
+
|
221
|
+
Sanitize::CSS.stylesheet(css).strip.must_equal %[
|
222
|
+
.foo { }
|
223
|
+
#bar { }
|
224
|
+
].strip
|
225
|
+
|
226
|
+
Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED[:css]).must_equal css
|
227
|
+
|
228
|
+
Sanitize::CSS.stylesheet(css, :properties => %w[background color width]).strip.must_equal %[
|
229
|
+
.foo { color: #fff; }
|
230
|
+
#bar { }
|
231
|
+
].strip
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
describe '.tree!' do
|
236
|
+
it 'should sanitize a Crass CSS parse tree with the given config' do
|
237
|
+
tree = Crass.parse(String.new("@import url(foo.css);\n") <<
|
238
|
+
".foo { background: #fff; font: 16pt 'Comic Sans MS'; }\n" <<
|
239
|
+
"#bar { top: 125px; background: green; }")
|
240
|
+
|
241
|
+
Sanitize::CSS.tree!(tree, :properties => %w[background color width]).must_be_same_as tree
|
242
|
+
|
243
|
+
Crass::Parser.stringify(tree).must_equal String.new("\n") <<
|
244
|
+
".foo { background: #fff; }\n" <<
|
245
|
+
"#bar { background: green; }"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
describe 'functionality' do
|
251
|
+
before do
|
252
|
+
@default = Sanitize::CSS.new
|
253
|
+
@relaxed = Sanitize::CSS.new(Sanitize::Config::RELAXED[:css])
|
254
|
+
end
|
255
|
+
|
256
|
+
# https://github.com/rgrove/sanitize/issues/121
|
257
|
+
it 'should parse the contents of @media rules properly' do
|
258
|
+
css = '@media { p[class="center"] { text-align: center; }}'
|
259
|
+
@relaxed.stylesheet(css).must_equal css
|
260
|
+
|
261
|
+
css = %[
|
262
|
+
@media (max-width: 720px) {
|
263
|
+
p.foo > .bar { float: right; width: expression(body.scrollLeft + 50 + 'px'); }
|
264
|
+
#baz { color: green; }
|
265
|
+
|
266
|
+
@media (orientation: portrait) {
|
267
|
+
#baz { color: red; }
|
268
|
+
}
|
269
|
+
}
|
270
|
+
].strip
|
271
|
+
|
272
|
+
@relaxed.stylesheet(css).must_equal %[
|
273
|
+
@media (max-width: 720px) {
|
274
|
+
p.foo > .bar { float: right; }
|
275
|
+
#baz { color: green; }
|
276
|
+
|
277
|
+
@media (orientation: portrait) {
|
278
|
+
#baz { color: red; }
|
279
|
+
}
|
280
|
+
}
|
281
|
+
].strip
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'should parse @page rules properly' do
|
285
|
+
css = %[
|
286
|
+
@page { margin: 2cm } /* All margins set to 2cm */
|
287
|
+
|
288
|
+
@page :right {
|
289
|
+
@top-center { content: "Preliminary edition" }
|
290
|
+
@bottom-center { content: counter(page) }
|
291
|
+
}
|
292
|
+
|
293
|
+
@page {
|
294
|
+
size: 8.5in 11in;
|
295
|
+
margin: 10%;
|
296
|
+
|
297
|
+
@top-left {
|
298
|
+
content: "Hamlet";
|
299
|
+
}
|
300
|
+
@top-right {
|
301
|
+
content: "Page " counter(page);
|
302
|
+
}
|
303
|
+
}
|
304
|
+
].strip
|
305
|
+
|
306
|
+
@relaxed.stylesheet(css).must_equal css
|
307
|
+
end
|
308
|
+
|
309
|
+
describe ":at_rules" do
|
310
|
+
it "should remove blockless at-rules that aren't allowlisted" do
|
311
|
+
css = %[
|
312
|
+
@charset 'utf-8';
|
313
|
+
@import url('foo.css');
|
314
|
+
.foo { color: green; }
|
315
|
+
].strip
|
316
|
+
|
317
|
+
@relaxed.stylesheet(css).strip.must_equal %[
|
318
|
+
.foo { color: green; }
|
319
|
+
].strip
|
320
|
+
end
|
321
|
+
|
322
|
+
describe "when blockless at-rules are allowlisted" do
|
323
|
+
before do
|
324
|
+
@scss = Sanitize::CSS.new(Sanitize::Config.merge(Sanitize::Config::RELAXED[:css], {
|
325
|
+
:at_rules => ['charset', 'import']
|
326
|
+
}))
|
327
|
+
end
|
328
|
+
|
329
|
+
it "should not remove them" do
|
330
|
+
css = %[
|
331
|
+
@charset 'utf-8';
|
332
|
+
@import url('foo.css');
|
333
|
+
.foo { color: green; }
|
334
|
+
].strip
|
335
|
+
|
336
|
+
@scss.stylesheet(css).must_equal %[
|
337
|
+
@charset 'utf-8';
|
338
|
+
@import url('foo.css');
|
339
|
+
.foo { color: green; }
|
340
|
+
].strip
|
341
|
+
end
|
342
|
+
|
343
|
+
it "should remove them if they have invalid blocks" do
|
344
|
+
css = %[
|
345
|
+
@charset { color: green }
|
346
|
+
@import { color: green }
|
347
|
+
.foo { color: green; }
|
348
|
+
].strip
|
349
|
+
|
350
|
+
@scss.stylesheet(css).strip.must_equal %[
|
351
|
+
.foo { color: green; }
|
352
|
+
].strip
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
describe "when validating @import rules" do
|
357
|
+
|
358
|
+
describe "with no validation proc specified" do
|
359
|
+
before do
|
360
|
+
@scss = Sanitize::CSS.new(Sanitize::Config.merge(Sanitize::Config::RELAXED[:css], {
|
361
|
+
:at_rules => ['import']
|
362
|
+
}))
|
363
|
+
end
|
364
|
+
|
365
|
+
it "should allow any URL value" do
|
366
|
+
css = %[
|
367
|
+
@import url('https://somesite.com/something.css');
|
368
|
+
].strip
|
369
|
+
|
370
|
+
@scss.stylesheet(css).strip.must_equal %[
|
371
|
+
@import url('https://somesite.com/something.css');
|
372
|
+
].strip
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
describe "with a validation proc specified" do
|
377
|
+
before do
|
378
|
+
google_font_validator = Proc.new { |url| url.start_with?("https://fonts.googleapis.com") }
|
379
|
+
|
380
|
+
@scss = Sanitize::CSS.new(Sanitize::Config.merge(Sanitize::Config::RELAXED[:css], {
|
381
|
+
:at_rules => ['import'], :import_url_validator => google_font_validator
|
382
|
+
}))
|
383
|
+
end
|
384
|
+
|
385
|
+
it "should allow a google fonts url" do
|
386
|
+
css = %[
|
387
|
+
@import 'https://fonts.googleapis.com/css?family=Indie+Flower';
|
388
|
+
@import url('https://fonts.googleapis.com/css?family=Indie+Flower');
|
389
|
+
].strip
|
390
|
+
|
391
|
+
@scss.stylesheet(css).strip.must_equal %[
|
392
|
+
@import 'https://fonts.googleapis.com/css?family=Indie+Flower';
|
393
|
+
@import url('https://fonts.googleapis.com/css?family=Indie+Flower');
|
394
|
+
].strip
|
395
|
+
end
|
396
|
+
|
397
|
+
it "should not allow a nasty url" do
|
398
|
+
css = %[
|
399
|
+
@import 'https://fonts.googleapis.com/css?family=Indie+Flower';
|
400
|
+
@import 'https://nastysite.com/nasty_hax0r.css';
|
401
|
+
@import url('https://nastysite.com/nasty_hax0r.css');
|
402
|
+
].strip
|
403
|
+
|
404
|
+
@scss.stylesheet(css).strip.must_equal %[
|
405
|
+
@import 'https://fonts.googleapis.com/css?family=Indie+Flower';
|
406
|
+
].strip
|
407
|
+
end
|
408
|
+
|
409
|
+
it "should not allow a blank url" do
|
410
|
+
css = %[
|
411
|
+
@import 'https://fonts.googleapis.com/css?family=Indie+Flower';
|
412
|
+
@import '';
|
413
|
+
@import url('');
|
414
|
+
].strip
|
415
|
+
|
416
|
+
@scss.stylesheet(css).strip.must_equal %[
|
417
|
+
@import 'https://fonts.googleapis.com/css?family=Indie+Flower';
|
418
|
+
].strip
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative 'common'
|
3
|
+
|
4
|
+
describe 'Transformers' do
|
5
|
+
make_my_diffs_pretty!
|
6
|
+
parallelize_me!
|
7
|
+
|
8
|
+
it 'should receive a complete env Hash as input' do
|
9
|
+
Sanitize.fragment('<SPAN>foo</SPAN>',
|
10
|
+
:foo => :bar,
|
11
|
+
:transformers => lambda {|env|
|
12
|
+
return unless env[:node].element?
|
13
|
+
|
14
|
+
env[:config][:foo].must_equal :bar
|
15
|
+
env[:is_allowlisted].must_equal false
|
16
|
+
env[:is_whitelisted].must_equal env[:is_allowlisted]
|
17
|
+
env[:node].must_be_kind_of Nokogiri::XML::Node
|
18
|
+
env[:node_name].must_equal 'span'
|
19
|
+
env[:node_allowlist].must_be_kind_of Set
|
20
|
+
env[:node_allowlist].must_be_empty
|
21
|
+
env[:node_whitelist].must_equal env[:node_allowlist]
|
22
|
+
}
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should traverse all node types, including the fragment itself' do
|
27
|
+
nodes = []
|
28
|
+
|
29
|
+
Sanitize.fragment('<div>foo</div><!--bar--><script>cdata!</script>',
|
30
|
+
:transformers => proc {|env| nodes << env[:node_name] }
|
31
|
+
)
|
32
|
+
|
33
|
+
nodes.must_equal %w[
|
34
|
+
#document-fragment div text text text comment script text
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should perform top-down traversal' do
|
39
|
+
nodes = []
|
40
|
+
|
41
|
+
Sanitize.fragment('<div><span><strong>foo</strong></span><b></b></div><p>bar</p>',
|
42
|
+
:transformers => proc {|env| nodes << env[:node_name] if env[:node].element? }
|
43
|
+
)
|
44
|
+
|
45
|
+
nodes.must_equal %w[div span strong b p]
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should allowlist nodes in the node allowlist' do
|
49
|
+
Sanitize.fragment('<div class="foo">foo</div><span>bar</span>',
|
50
|
+
:transformers => [
|
51
|
+
proc {|env|
|
52
|
+
{:node_allowlist => [env[:node]]} if env[:node_name] == 'div'
|
53
|
+
},
|
54
|
+
|
55
|
+
proc {|env|
|
56
|
+
env[:is_allowlisted].must_equal false unless env[:node_name] == 'div'
|
57
|
+
env[:is_allowlisted].must_equal true if env[:node_name] == 'div'
|
58
|
+
env[:node_allowlist].must_include env[:node] if env[:node_name] == 'div'
|
59
|
+
env[:is_whitelisted].must_equal env[:is_allowlisted]
|
60
|
+
env[:node_whitelist].must_equal env[:node_allowlist]
|
61
|
+
}
|
62
|
+
]
|
63
|
+
).must_equal '<div class="foo">foo</div>bar'
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should clear the node allowlist after each fragment' do
|
67
|
+
called = false
|
68
|
+
|
69
|
+
Sanitize.fragment('<div>foo</div>',
|
70
|
+
:transformers => proc {|env| {:node_allowlist => [env[:node]]}}
|
71
|
+
)
|
72
|
+
|
73
|
+
Sanitize.fragment('<div>foo</div>',
|
74
|
+
:transformers => proc {|env|
|
75
|
+
called = true
|
76
|
+
env[:is_allowlisted].must_equal false
|
77
|
+
env[:is_whitelisted].must_equal env[:is_allowlisted]
|
78
|
+
env[:node_allowlist].must_be_empty
|
79
|
+
env[:node_whitelist].must_equal env[:node_allowlist]
|
80
|
+
}
|
81
|
+
)
|
82
|
+
|
83
|
+
called.must_equal true
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should accept a method transformer' do
|
87
|
+
def transformer(env); end
|
88
|
+
Sanitize.fragment('<div>foo</div>', :transformers => method(:transformer))
|
89
|
+
.must_equal(' foo ')
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'Image allowlist transformer' do
|
93
|
+
require 'uri'
|
94
|
+
|
95
|
+
image_allowlist_transformer = lambda do |env|
|
96
|
+
# Ignore everything except <img> elements.
|
97
|
+
return unless env[:node_name] == 'img'
|
98
|
+
|
99
|
+
node = env[:node]
|
100
|
+
image_uri = URI.parse(node['src'])
|
101
|
+
|
102
|
+
# Only allow relative URLs or URLs with the example.com domain. The
|
103
|
+
# image_uri.host.nil? check ensures that protocol-relative URLs like
|
104
|
+
# "//evil.com/foo.jpg".
|
105
|
+
unless image_uri.host == 'example.com' || (image_uri.host.nil? && image_uri.relative?)
|
106
|
+
node.unlink # `Nokogiri::XML::Node#unlink` removes a node from the document
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
before do
|
111
|
+
@s = Sanitize.new(Sanitize::Config.merge(Sanitize::Config::RELAXED,
|
112
|
+
:transformers => image_allowlist_transformer))
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should allow images with relative URLs' do
|
116
|
+
input = '<img src="/foo/bar.jpg">'
|
117
|
+
@s.fragment(input).must_equal(input)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should allow images at the example.com domain' do
|
121
|
+
input = '<img src="http://example.com/foo/bar.jpg">'
|
122
|
+
@s.fragment(input).must_equal(input)
|
123
|
+
|
124
|
+
input = '<img src="https://example.com/foo/bar.jpg">'
|
125
|
+
@s.fragment(input).must_equal(input)
|
126
|
+
|
127
|
+
input = '<img src="//example.com/foo/bar.jpg">'
|
128
|
+
@s.fragment(input).must_equal(input)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should not allow images at other domains' do
|
132
|
+
input = '<img src="http://evil.com/foo/bar.jpg">'
|
133
|
+
@s.fragment(input).must_equal('')
|
134
|
+
|
135
|
+
input = '<img src="https://evil.com/foo/bar.jpg">'
|
136
|
+
@s.fragment(input).must_equal('')
|
137
|
+
|
138
|
+
input = '<img src="//evil.com/foo/bar.jpg">'
|
139
|
+
@s.fragment(input).must_equal('')
|
140
|
+
|
141
|
+
input = '<img src="http://subdomain.example.com/foo/bar.jpg">'
|
142
|
+
@s.fragment(input).must_equal('')
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe 'YouTube transformer' do
|
147
|
+
youtube_transformer = lambda do |env|
|
148
|
+
node = env[:node]
|
149
|
+
node_name = env[:node_name]
|
150
|
+
|
151
|
+
# Don't continue if this node is already allowlisted or is not an element.
|
152
|
+
return if env[:is_allowlisted] || !node.element?
|
153
|
+
|
154
|
+
# Don't continue unless the node is an iframe.
|
155
|
+
return unless node_name == 'iframe'
|
156
|
+
|
157
|
+
# Verify that the video URL is actually a valid YouTube video URL.
|
158
|
+
return unless node['src'] =~ %r|\A(?:https?:)?//(?:www\.)?youtube(?:-nocookie)?\.com/|
|
159
|
+
|
160
|
+
# We're now certain that this is a YouTube embed, but we still need to run
|
161
|
+
# it through a special Sanitize step to ensure that no unwanted elements or
|
162
|
+
# attributes that don't belong in a YouTube embed can sneak in.
|
163
|
+
Sanitize.node!(node, {
|
164
|
+
:elements => %w[iframe],
|
165
|
+
|
166
|
+
:attributes => {
|
167
|
+
'iframe' => %w[allowfullscreen frameborder height src width]
|
168
|
+
}
|
169
|
+
})
|
170
|
+
|
171
|
+
# Now that we're sure that this is a valid YouTube embed and that there are
|
172
|
+
# no unwanted elements or attributes hidden inside it, we can tell Sanitize
|
173
|
+
# to allowlist the current node.
|
174
|
+
{:node_allowlist => [node]}
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'should allow HTTP YouTube video embeds' do
|
178
|
+
input = '<iframe width="420" height="315" src="http://www.youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen bogus="bogus"><script>alert()</script></iframe>'
|
179
|
+
|
180
|
+
Sanitize.fragment(input, :transformers => youtube_transformer)
|
181
|
+
.must_equal '<iframe width="420" height="315" src="http://www.youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen=""></iframe>'
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'should allow HTTPS YouTube video embeds' do
|
185
|
+
input = '<iframe width="420" height="315" src="https://www.youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen bogus="bogus"><script>alert()</script></iframe>'
|
186
|
+
|
187
|
+
Sanitize.fragment(input, :transformers => youtube_transformer)
|
188
|
+
.must_equal '<iframe width="420" height="315" src="https://www.youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen=""></iframe>'
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'should allow protocol-relative YouTube video embeds' do
|
192
|
+
input = '<iframe width="420" height="315" src="//www.youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen bogus="bogus"><script>alert()</script></iframe>'
|
193
|
+
|
194
|
+
Sanitize.fragment(input, :transformers => youtube_transformer)
|
195
|
+
.must_equal '<iframe width="420" height="315" src="//www.youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen=""></iframe>'
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'should allow privacy-enhanced YouTube video embeds' do
|
199
|
+
input = '<iframe width="420" height="315" src="https://www.youtube-nocookie.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen bogus="bogus"><script>alert()</script></iframe>'
|
200
|
+
|
201
|
+
Sanitize.fragment(input, :transformers => youtube_transformer)
|
202
|
+
.must_equal '<iframe width="420" height="315" src="https://www.youtube-nocookie.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen=""></iframe>'
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should not allow non-YouTube video embeds' do
|
206
|
+
input = '<iframe width="420" height="315" src="http://www.fake-youtube.com/embed/QH2-TGUlwu4" frameborder="0" allowfullscreen></iframe>'
|
207
|
+
|
208
|
+
Sanitize.fragment(input, :transformers => youtube_transformer)
|
209
|
+
.must_equal('')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe 'DOM modification transformer' do
|
214
|
+
b_to_strong_tag_transformer = lambda do |env|
|
215
|
+
node = env[:node]
|
216
|
+
node_name = env[:node_name]
|
217
|
+
|
218
|
+
if node_name == 'b'
|
219
|
+
node.name = 'strong'
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'should allow the <b> tag to be changed to a <strong> tag' do
|
224
|
+
input = '<b>text</b>'
|
225
|
+
|
226
|
+
Sanitize.fragment(input, :elements => ['strong'], :transformers => b_to_strong_tag_transformer)
|
227
|
+
.must_equal '<strong>text</strong>'
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|