rails-html-sanitizer 1.5.0 → 1.6.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 +125 -0
- data/MIT-LICENSE +1 -1
- data/README.md +124 -72
- data/lib/rails/html/sanitizer/version.rb +4 -2
- data/lib/rails/html/sanitizer.rb +372 -104
- data/lib/rails/html/scrubbers.rb +98 -73
- data/lib/rails-html-sanitizer.rb +7 -23
- data/test/rails_api_test.rb +88 -0
- data/test/sanitizer_test.rb +1095 -584
- data/test/scrubbers_test.rb +129 -38
- metadata +68 -58
data/test/sanitizer_test.rb
CHANGED
@@ -1,777 +1,1288 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "minitest/autorun"
|
2
4
|
require "rails-html-sanitizer"
|
3
|
-
require "rails/dom/testing/assertions/dom_assertions"
|
4
5
|
|
5
|
-
puts Nokogiri::VERSION_INFO
|
6
|
+
puts "nokogiri version info: #{Nokogiri::VERSION_INFO}"
|
7
|
+
puts "html5 support: #{Rails::HTML::Sanitizer.html5_support?}"
|
8
|
+
|
9
|
+
#
|
10
|
+
# NOTE that many of these tests contain multiple acceptable results.
|
11
|
+
#
|
12
|
+
# In some cases, this is because of how the HTML4 parser's recovery behavior changed in libxml2
|
13
|
+
# 2.9.14 and 2.10.0. For more details, see:
|
14
|
+
#
|
15
|
+
# - https://github.com/sparklemotion/nokogiri/releases/tag/v1.13.5
|
16
|
+
# - https://gitlab.gnome.org/GNOME/libxml2/-/issues/380
|
17
|
+
#
|
18
|
+
# In other cases, multiple acceptable results are provided because Nokogiri's vendored libxml2 is
|
19
|
+
# patched to entity-escape server-side includes (aks "SSI", aka `<!-- #directive param=value -->`).
|
20
|
+
#
|
21
|
+
# In many other cases, it's because the parser used by Nokogiri on JRuby (xerces+nekohtml) parses
|
22
|
+
# slightly differently than libxml2 in edge cases.
|
23
|
+
#
|
24
|
+
module SanitizerTests
|
25
|
+
def self.loofah_html5_support?
|
26
|
+
Loofah.respond_to?(:html5_support?) && Loofah.html5_support?
|
27
|
+
end
|
28
|
+
|
29
|
+
class BaseSanitizerTest < Minitest::Test
|
30
|
+
class XpathRemovalTestSanitizer < Rails::HTML::Sanitizer
|
31
|
+
def sanitize(html, options = {})
|
32
|
+
fragment = Loofah.fragment(html)
|
33
|
+
remove_xpaths(fragment, options[:xpaths]).to_s
|
34
|
+
end
|
35
|
+
end
|
6
36
|
|
7
|
-
|
8
|
-
|
37
|
+
def test_sanitizer_sanitize_raises_not_implemented_error
|
38
|
+
assert_raises NotImplementedError do
|
39
|
+
Rails::HTML::Sanitizer.new.sanitize("asdf")
|
40
|
+
end
|
41
|
+
end
|
9
42
|
|
10
|
-
|
11
|
-
|
12
|
-
|
43
|
+
def test_remove_xpaths_removes_an_xpath
|
44
|
+
html = %(<h1>hello <script>code!</script></h1>)
|
45
|
+
assert_equal %(<h1>hello </h1>), xpath_sanitize(html, xpaths: %w(.//script))
|
13
46
|
end
|
14
|
-
end
|
15
47
|
|
16
|
-
|
17
|
-
|
18
|
-
|
48
|
+
def test_remove_xpaths_removes_all_occurrences_of_xpath
|
49
|
+
html = %(<section><header><script>code!</script></header><p>hello <script>code!</script></p></section>)
|
50
|
+
assert_equal %(<section><header></header><p>hello </p></section>), xpath_sanitize(html, xpaths: %w(.//script))
|
51
|
+
end
|
19
52
|
|
20
|
-
|
21
|
-
|
22
|
-
|
53
|
+
def test_remove_xpaths_called_with_faulty_xpath
|
54
|
+
assert_raises Nokogiri::XML::XPath::SyntaxError do
|
55
|
+
xpath_sanitize("<h1>hello<h1>", xpaths: %w(..faulty_xpath))
|
56
|
+
end
|
57
|
+
end
|
23
58
|
|
24
|
-
|
25
|
-
|
26
|
-
fragment = Loofah.fragment(html)
|
27
|
-
remove_xpaths(fragment, options[:xpaths]).to_s
|
59
|
+
def test_remove_xpaths_called_with_xpath_string
|
60
|
+
assert_equal "", xpath_sanitize("<a></a>", xpaths: ".//a")
|
28
61
|
end
|
29
|
-
end
|
30
62
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
end
|
63
|
+
def test_remove_xpaths_called_with_enumerable_xpaths
|
64
|
+
assert_equal "", xpath_sanitize("<a><span></span></a>", xpaths: %w(.//a .//span))
|
65
|
+
end
|
35
66
|
|
36
|
-
|
37
|
-
|
38
|
-
|
67
|
+
protected
|
68
|
+
def xpath_sanitize(input, options = {})
|
69
|
+
XpathRemovalTestSanitizer.new.sanitize(input, options)
|
70
|
+
end
|
39
71
|
end
|
40
72
|
|
41
|
-
|
42
|
-
|
43
|
-
|
73
|
+
module ModuleUnderTest
|
74
|
+
def module_under_test
|
75
|
+
self.class.instance_variable_get(:@module_under_test)
|
44
76
|
end
|
45
77
|
end
|
46
78
|
|
47
|
-
|
48
|
-
|
49
|
-
end
|
79
|
+
module FullSanitizerTest
|
80
|
+
include ModuleUnderTest
|
50
81
|
|
51
|
-
|
52
|
-
|
53
|
-
|
82
|
+
def test_strip_tags_with_quote
|
83
|
+
input = '<" <img src="trollface.gif" onload="alert(1)"> hi'
|
84
|
+
result = full_sanitize(input)
|
85
|
+
acceptable_results = [
|
86
|
+
# libxml2 >= 2.9.14 and xerces+neko
|
87
|
+
%{<" hi},
|
88
|
+
# other libxml2
|
89
|
+
%{ hi},
|
90
|
+
]
|
54
91
|
|
55
|
-
|
56
|
-
|
57
|
-
expected = libxml_2_9_14_recovery_lt? ? %{<" hi} : %{ hi}
|
58
|
-
assert_equal(expected, full_sanitize(input))
|
59
|
-
end
|
92
|
+
assert_includes(acceptable_results, result)
|
93
|
+
end
|
60
94
|
|
61
|
-
|
62
|
-
|
63
|
-
|
95
|
+
def test_strip_invalid_html
|
96
|
+
assert_equal "<<", full_sanitize("<<<bad html")
|
97
|
+
end
|
64
98
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
99
|
+
def test_strip_nested_tags
|
100
|
+
expected = "Wei<a onclick='alert(document.cookie);'/>rdos"
|
101
|
+
input = "Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"
|
102
|
+
assert_equal expected, full_sanitize(input)
|
103
|
+
end
|
70
104
|
|
71
|
-
|
72
|
-
|
73
|
-
|
105
|
+
def test_strip_tags_multiline
|
106
|
+
expected = %{This is a test.\n\n\n\nIt no longer contains any HTML.\n}
|
107
|
+
input = %{<h1>This is <b>a <a href="" target="_blank">test</a></b>.</h1>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}
|
74
108
|
|
75
|
-
|
76
|
-
|
109
|
+
assert_equal expected, full_sanitize(input)
|
110
|
+
end
|
77
111
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
112
|
+
def test_remove_unclosed_tags
|
113
|
+
input = "This is <-- not\n a comment here."
|
114
|
+
result = full_sanitize(input)
|
115
|
+
acceptable_results = [
|
116
|
+
# libxml2 >= 2.9.14 and xerces+neko
|
117
|
+
%{This is <-- not\n a comment here.},
|
118
|
+
# other libxml2
|
119
|
+
%{This is },
|
120
|
+
]
|
121
|
+
|
122
|
+
assert_includes(acceptable_results, result)
|
123
|
+
end
|
83
124
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
125
|
+
def test_strip_cdata
|
126
|
+
input = "This has a <![CDATA[<section>]]> here."
|
127
|
+
result = full_sanitize(input)
|
128
|
+
acceptable_results = [
|
129
|
+
# libxml2 = 2.9.14
|
130
|
+
%{This has a <![CDATA[]]> here.},
|
131
|
+
# other libxml2
|
132
|
+
%{This has a ]]> here.},
|
133
|
+
# xerces+neko
|
134
|
+
%{This has a here.},
|
135
|
+
]
|
136
|
+
|
137
|
+
assert_includes(acceptable_results, result)
|
138
|
+
end
|
89
139
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
140
|
+
def test_strip_blank_string
|
141
|
+
assert_nil full_sanitize(nil)
|
142
|
+
assert_equal "", full_sanitize("")
|
143
|
+
assert_equal " ", full_sanitize(" ")
|
144
|
+
end
|
95
145
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
assert_equal " ", full_sanitize(" ")
|
100
|
-
end
|
146
|
+
def test_strip_tags_with_plaintext
|
147
|
+
assert_equal "Don't touch me", full_sanitize("Don't touch me")
|
148
|
+
end
|
101
149
|
|
102
|
-
|
103
|
-
|
104
|
-
|
150
|
+
def test_strip_tags_with_tags
|
151
|
+
assert_equal "This is a test.", full_sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>")
|
152
|
+
end
|
105
153
|
|
106
|
-
|
107
|
-
|
108
|
-
|
154
|
+
def test_escape_tags_with_many_open_quotes
|
155
|
+
assert_equal "<<", full_sanitize("<<<bad html>")
|
156
|
+
end
|
109
157
|
|
110
|
-
|
111
|
-
|
112
|
-
|
158
|
+
def test_strip_tags_with_sentence
|
159
|
+
assert_equal "This is a test.", full_sanitize("This is a test.")
|
160
|
+
end
|
113
161
|
|
114
|
-
|
115
|
-
|
116
|
-
|
162
|
+
def test_strip_tags_with_comment
|
163
|
+
assert_equal "This has a here.", full_sanitize("This has a <!-- comment --> here.")
|
164
|
+
end
|
117
165
|
|
118
|
-
|
119
|
-
|
120
|
-
|
166
|
+
def test_strip_tags_with_frozen_string
|
167
|
+
assert_equal "Frozen string with no tags", full_sanitize("Frozen string with no tags")
|
168
|
+
end
|
121
169
|
|
122
|
-
|
123
|
-
|
124
|
-
|
170
|
+
def test_full_sanitize_respect_html_escaping_of_the_given_string
|
171
|
+
assert_equal 'test\r\nstring', full_sanitize('test\r\nstring')
|
172
|
+
assert_equal "&", full_sanitize("&")
|
173
|
+
assert_equal "&", full_sanitize("&")
|
174
|
+
assert_equal "&amp;", full_sanitize("&amp;")
|
175
|
+
assert_equal "omg <script>BOM</script>", full_sanitize("omg <script>BOM</script>")
|
176
|
+
end
|
125
177
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
end
|
178
|
+
def test_sanitize_ascii_8bit_string
|
179
|
+
full_sanitize("<div><a>hello</a></div>".encode("ASCII-8BIT")).tap do |sanitized|
|
180
|
+
assert_equal "hello", sanitized
|
181
|
+
assert_equal Encoding::UTF_8, sanitized.encoding
|
182
|
+
end
|
183
|
+
end
|
133
184
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
185
|
+
protected
|
186
|
+
def full_sanitize(input, options = {})
|
187
|
+
module_under_test::FullSanitizer.new.sanitize(input, options)
|
188
|
+
end
|
138
189
|
end
|
139
190
|
|
140
|
-
|
141
|
-
|
191
|
+
class HTML4FullSanitizerTest < Minitest::Test
|
192
|
+
@module_under_test = Rails::HTML4
|
193
|
+
include FullSanitizerTest
|
142
194
|
end
|
143
195
|
|
144
|
-
|
145
|
-
|
146
|
-
|
196
|
+
class HTML5FullSanitizerTest < Minitest::Test
|
197
|
+
@module_under_test = Rails::HTML5
|
198
|
+
include FullSanitizerTest
|
199
|
+
end if loofah_html5_support?
|
147
200
|
|
148
|
-
|
149
|
-
|
150
|
-
end
|
201
|
+
module LinkSanitizerTest
|
202
|
+
include ModuleUnderTest
|
151
203
|
|
152
|
-
|
153
|
-
|
154
|
-
|
204
|
+
def test_strip_links_with_tags_in_tags
|
205
|
+
expected = "<a href='hello'>all <b>day</b> long</a>"
|
206
|
+
input = "<<a>a href='hello'>all <b>day</b> long<</A>/a>"
|
207
|
+
assert_equal expected, link_sanitize(input)
|
208
|
+
end
|
155
209
|
|
156
|
-
|
157
|
-
|
158
|
-
|
210
|
+
def test_strip_links_with_unclosed_tags
|
211
|
+
assert_equal "", link_sanitize("<a<a")
|
212
|
+
end
|
159
213
|
|
160
|
-
|
161
|
-
|
162
|
-
|
214
|
+
def test_strip_links_with_plaintext
|
215
|
+
assert_equal "Don't touch me", link_sanitize("Don't touch me")
|
216
|
+
end
|
163
217
|
|
164
|
-
|
165
|
-
|
166
|
-
|
218
|
+
def test_strip_links_with_line_feed_and_uppercase_tag
|
219
|
+
assert_equal "on my mind\nall day long", link_sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
|
220
|
+
end
|
167
221
|
|
168
|
-
|
169
|
-
|
170
|
-
|
222
|
+
def test_strip_links_leaves_nonlink_tags
|
223
|
+
assert_equal "My mind\nall <b>day</b> long", link_sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
|
224
|
+
end
|
171
225
|
|
172
|
-
|
173
|
-
|
174
|
-
|
226
|
+
def test_strip_links_with_links
|
227
|
+
assert_equal "0wn3d", link_sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
|
228
|
+
end
|
175
229
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
end
|
230
|
+
def test_strip_links_with_linkception
|
231
|
+
assert_equal "Magic", link_sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
|
232
|
+
end
|
180
233
|
|
181
|
-
|
182
|
-
|
183
|
-
|
234
|
+
def test_sanitize_ascii_8bit_string
|
235
|
+
link_sanitize("<div><a>hello</a></div>".encode("ASCII-8BIT")).tap do |sanitized|
|
236
|
+
assert_equal "<div>hello</div>", sanitized
|
237
|
+
assert_equal Encoding::UTF_8, sanitized.encoding
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
protected
|
242
|
+
def link_sanitize(input, options = {})
|
243
|
+
module_under_test::LinkSanitizer.new.sanitize(input, options)
|
244
|
+
end
|
184
245
|
end
|
185
246
|
|
186
|
-
|
187
|
-
|
188
|
-
|
247
|
+
class HTML4LinkSanitizerTest < Minitest::Test
|
248
|
+
@module_under_test = Rails::HTML4
|
249
|
+
include LinkSanitizerTest
|
189
250
|
end
|
190
251
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
252
|
+
class HTML5LinkSanitizerTest < Minitest::Test
|
253
|
+
@module_under_test = Rails::HTML5
|
254
|
+
include LinkSanitizerTest
|
255
|
+
end if loofah_html5_support?
|
256
|
+
|
257
|
+
module SafeListSanitizerTest
|
258
|
+
include ModuleUnderTest
|
259
|
+
|
260
|
+
def test_sanitize_nested_script
|
261
|
+
assert_equal '<script>alert("XSS");</script>', safe_list_sanitize('<script><script></script>alert("XSS");<script><</script>/</script><script>script></script>', tags: %w(em))
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_sanitize_nested_script_in_style
|
265
|
+
input = '<style><script></style>alert("XSS");<style><</style>/</style><style>script></style>'
|
266
|
+
result = safe_list_sanitize(input, tags: %w(em))
|
267
|
+
acceptable_results = [
|
268
|
+
# libxml2
|
269
|
+
%{<script>alert("XSS");</script>},
|
270
|
+
# xerces+neko. unavoidable double-escaping, see loofah/docs/2022-10-decision-on-cdata-nodes.md
|
271
|
+
%{&lt;script&gt;alert(\"XSS\");&lt;&lt;/style&gt;/script&gt;},
|
272
|
+
]
|
273
|
+
|
274
|
+
assert_includes(acceptable_results, result)
|
275
|
+
end
|
276
|
+
|
277
|
+
def test_strip_unclosed_cdata
|
278
|
+
input = "This has an unclosed <![CDATA[<section>]] here..."
|
279
|
+
|
280
|
+
result = safe_list_sanitize(input)
|
281
|
+
|
282
|
+
acceptable_results = [
|
283
|
+
# libxml2 = 2.9.14
|
284
|
+
%{This has an unclosed <![CDATA[]] here...},
|
285
|
+
# other libxml2
|
286
|
+
%{This has an unclosed ]] here...},
|
287
|
+
# xerces+neko
|
288
|
+
%{This has an unclosed }
|
289
|
+
]
|
290
|
+
|
291
|
+
assert_includes(acceptable_results, result)
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_sanitize_form
|
295
|
+
assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", ""
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_sanitize_plaintext
|
299
|
+
# note that the `plaintext` tag has been deprecated since HTML 2
|
300
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/plaintext
|
301
|
+
input = "<plaintext><span>foo</span></plaintext>"
|
302
|
+
result = safe_list_sanitize(input)
|
303
|
+
acceptable_results = [
|
304
|
+
# libxml2
|
305
|
+
"<span>foo</span>",
|
306
|
+
# xerces+nekohtml-unit
|
307
|
+
"<span>foo</span></plaintext>",
|
308
|
+
# xerces+cyberneko
|
309
|
+
"<span>foo</span>"
|
310
|
+
]
|
311
|
+
|
312
|
+
assert_includes(acceptable_results, result)
|
313
|
+
end
|
314
|
+
|
315
|
+
def test_sanitize_script
|
316
|
+
assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cblah blah blahd e f"
|
317
|
+
end
|
318
|
+
|
319
|
+
def test_sanitize_js_handlers
|
320
|
+
raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>}
|
321
|
+
assert_sanitized raw, %{onthis="do that" <a href="#" name="foo">hello</a>}
|
322
|
+
end
|
323
|
+
|
324
|
+
def test_sanitize_javascript_href
|
325
|
+
raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>}
|
326
|
+
assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>}
|
327
|
+
end
|
328
|
+
|
329
|
+
def test_sanitize_image_src
|
330
|
+
raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>}
|
331
|
+
assert_sanitized raw, %{src="javascript:bang" <img width="5">foo, <span>bar</span>}
|
332
|
+
end
|
333
|
+
|
334
|
+
def test_should_allow_anchors
|
335
|
+
assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href=\"foo\">baz</a>)
|
336
|
+
end
|
337
|
+
|
338
|
+
def test_video_poster_sanitization
|
339
|
+
scope_allowed_tags(%w(video)) do
|
340
|
+
scope_allowed_attributes %w(src poster) do
|
341
|
+
expected = if RUBY_PLATFORM == "java"
|
342
|
+
# xerces+nekohtml alphabetizes the attributes! FML.
|
343
|
+
%(<video poster="posterimage.jpg" src="videofile.ogg"></video>)
|
344
|
+
else
|
345
|
+
%(<video src="videofile.ogg" poster="posterimage.jpg"></video>)
|
346
|
+
end
|
347
|
+
assert_sanitized(
|
348
|
+
%(<video src="videofile.ogg" autoplay poster="posterimage.jpg"></video>),
|
349
|
+
expected,
|
350
|
+
)
|
351
|
+
assert_sanitized(
|
352
|
+
%(<video src="videofile.ogg" poster=javascript:alert(1)></video>),
|
353
|
+
%(<video src="videofile.ogg"></video>),
|
354
|
+
)
|
355
|
+
end
|
196
356
|
end
|
197
357
|
end
|
198
|
-
end
|
199
358
|
|
200
|
-
|
201
|
-
|
202
|
-
|
359
|
+
# RFC 3986, sec 4.2
|
360
|
+
def test_allow_colons_in_path_component
|
361
|
+
assert_sanitized "<a href=\"./this:that\">foo</a>"
|
362
|
+
end
|
203
363
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
assert_sanitized %(<video src="videofile.ogg" autoplay poster="posterimage.jpg"></video>), %(<video src="videofile.ogg" poster="posterimage.jpg"></video>)
|
208
|
-
assert_sanitized %(<video src="videofile.ogg" poster=javascript:alert(1)></video>), %(<video src="videofile.ogg"></video>)
|
364
|
+
%w(src width height alt).each do |img_attr|
|
365
|
+
define_method "test_should_allow_image_#{img_attr}_attribute" do
|
366
|
+
assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo">)
|
209
367
|
end
|
210
368
|
end
|
211
|
-
end
|
212
369
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
370
|
+
def test_lang_and_xml_lang
|
371
|
+
# https://html.spec.whatwg.org/multipage/dom.html#the-lang-and-xml:lang-attributes
|
372
|
+
#
|
373
|
+
# 3.2.6.2 The lang and xml:lang attributes
|
374
|
+
#
|
375
|
+
# ... Authors must not use the lang attribute in the XML namespace on HTML elements in HTML
|
376
|
+
# documents. To ease migration to and from XML, authors may specify an attribute in no namespace
|
377
|
+
# with no prefix and with the literal localname "xml:lang" on HTML elements in HTML documents,
|
378
|
+
# but such attributes must only be specified if a lang attribute in no namespace is also
|
379
|
+
# specified, and both attributes must have the same value when compared in an ASCII
|
380
|
+
# case-insensitive manner.
|
381
|
+
input = expected = "<div lang=\"en\" xml:lang=\"en\">foo</div>"
|
382
|
+
assert_sanitized(input, expected)
|
383
|
+
end
|
217
384
|
|
218
|
-
|
219
|
-
|
220
|
-
assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
|
385
|
+
def test_should_handle_non_html
|
386
|
+
assert_sanitized "abc"
|
221
387
|
end
|
222
|
-
end
|
223
388
|
|
224
|
-
|
225
|
-
|
226
|
-
|
389
|
+
def test_should_handle_blank_text
|
390
|
+
assert_nil(safe_list_sanitize(nil))
|
391
|
+
assert_equal("", safe_list_sanitize(""))
|
392
|
+
assert_equal(" ", safe_list_sanitize(" "))
|
393
|
+
end
|
227
394
|
|
228
|
-
|
229
|
-
|
230
|
-
|
395
|
+
def test_setting_allowed_tags_affects_sanitization
|
396
|
+
scope_allowed_tags %w(u) do |sanitizer|
|
397
|
+
assert_equal "<u></u>", sanitizer.sanitize("<a><u></u></a>")
|
398
|
+
end
|
399
|
+
end
|
231
400
|
|
232
|
-
|
233
|
-
|
234
|
-
|
401
|
+
def test_setting_allowed_attributes_affects_sanitization
|
402
|
+
scope_allowed_attributes %w(foo) do |sanitizer|
|
403
|
+
input = '<a foo="hello" bar="world"></a>'
|
404
|
+
assert_equal '<a foo="hello"></a>', sanitizer.sanitize(input)
|
405
|
+
end
|
235
406
|
end
|
236
|
-
end
|
237
407
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
408
|
+
def test_custom_tags_overrides_allowed_tags
|
409
|
+
scope_allowed_tags %(u) do |sanitizer|
|
410
|
+
input = "<a><u></u></a>"
|
411
|
+
assert_equal "<a></a>", sanitizer.sanitize(input, tags: %w(a))
|
412
|
+
end
|
242
413
|
end
|
243
|
-
end
|
244
414
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
415
|
+
def test_custom_attributes_overrides_allowed_attributes
|
416
|
+
scope_allowed_attributes %(foo) do |sanitizer|
|
417
|
+
input = '<a foo="hello" bar="world"></a>'
|
418
|
+
assert_equal '<a bar="world"></a>', sanitizer.sanitize(input, attributes: %w(bar))
|
419
|
+
end
|
249
420
|
end
|
250
|
-
end
|
251
421
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
assert_equal
|
422
|
+
def test_should_allow_prune
|
423
|
+
sanitizer = module_under_test::SafeListSanitizer.new(prune: true)
|
424
|
+
text = "<u>leave me <b>now</b></u>"
|
425
|
+
assert_equal "<u>leave me </u>", sanitizer.sanitize(text, tags: %w(u))
|
256
426
|
end
|
257
|
-
end
|
258
427
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
end
|
428
|
+
def test_should_allow_custom_tags
|
429
|
+
text = "<u>foo</u>"
|
430
|
+
assert_equal text, safe_list_sanitize(text, tags: %w(u))
|
431
|
+
end
|
264
432
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
433
|
+
def test_should_allow_only_custom_tags
|
434
|
+
text = "<u>foo</u> with <i>bar</i>"
|
435
|
+
assert_equal "<u>foo</u> with bar", safe_list_sanitize(text, tags: %w(u))
|
436
|
+
end
|
269
437
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
438
|
+
def test_should_allow_custom_tags_with_attributes
|
439
|
+
text = %(<blockquote cite="http://example.com/">foo</blockquote>)
|
440
|
+
assert_equal text, safe_list_sanitize(text)
|
441
|
+
end
|
274
442
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
443
|
+
def test_should_allow_custom_tags_with_custom_attributes
|
444
|
+
text = %(<blockquote foo="bar">Lorem ipsum</blockquote>)
|
445
|
+
assert_equal text, safe_list_sanitize(text, attributes: ["foo"])
|
446
|
+
end
|
279
447
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
end
|
448
|
+
def test_scrub_style_if_style_attribute_option_is_passed
|
449
|
+
input = '<p style="color: #000; background-image: url(http://www.ragingplatypus.com/i/cam-full.jpg);"></p>'
|
450
|
+
actual = safe_list_sanitize(input, attributes: %w(style))
|
284
451
|
|
285
|
-
|
286
|
-
|
287
|
-
actual = safe_list_sanitize(input, attributes: %w(style))
|
288
|
-
assert_includes(['<p style="color: #000;"></p>', '<p style="color:#000;"></p>'], actual)
|
289
|
-
end
|
452
|
+
assert_includes(['<p style="color: #000;"></p>', '<p style="color:#000;"></p>'], actual)
|
453
|
+
end
|
290
454
|
|
291
|
-
|
292
|
-
|
293
|
-
|
455
|
+
def test_should_raise_argument_error_if_tags_is_not_enumerable
|
456
|
+
assert_raises ArgumentError do
|
457
|
+
safe_list_sanitize("<a>some html</a>", tags: "foo")
|
458
|
+
end
|
294
459
|
end
|
295
|
-
end
|
296
460
|
|
297
|
-
|
298
|
-
|
299
|
-
|
461
|
+
def test_should_raise_argument_error_if_attributes_is_not_enumerable
|
462
|
+
assert_raises ArgumentError do
|
463
|
+
safe_list_sanitize("<a>some html</a>", attributes: "foo")
|
464
|
+
end
|
300
465
|
end
|
301
|
-
end
|
302
466
|
|
303
|
-
|
304
|
-
|
305
|
-
|
467
|
+
def test_should_not_accept_non_loofah_inheriting_scrubber
|
468
|
+
scrubber = Object.new
|
469
|
+
def scrubber.scrub(node); node.name = "h1"; end
|
306
470
|
|
307
|
-
|
308
|
-
|
471
|
+
assert_raises Loofah::ScrubberNotFound do
|
472
|
+
safe_list_sanitize("<a>some html</a>", scrubber: scrubber)
|
473
|
+
end
|
309
474
|
end
|
310
|
-
end
|
311
475
|
|
312
|
-
|
313
|
-
|
314
|
-
|
476
|
+
def test_should_accept_loofah_inheriting_scrubber
|
477
|
+
scrubber = Loofah::Scrubber.new
|
478
|
+
def scrubber.scrub(node); node.replace("<h1>#{node.inner_html}</h1>"); end
|
315
479
|
|
316
|
-
|
317
|
-
|
318
|
-
|
480
|
+
html = "<script>hello!</script>"
|
481
|
+
assert_equal "<h1>hello!</h1>", safe_list_sanitize(html, scrubber: scrubber)
|
482
|
+
end
|
319
483
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
484
|
+
def test_should_accept_loofah_scrubber_that_wraps_a_block
|
485
|
+
scrubber = Loofah::Scrubber.new { |node| node.replace("<h1>#{node.inner_html}</h1>") }
|
486
|
+
html = "<script>hello!</script>"
|
487
|
+
assert_equal "<h1>hello!</h1>", safe_list_sanitize(html, scrubber: scrubber)
|
488
|
+
end
|
325
489
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
490
|
+
def test_custom_scrubber_takes_precedence_over_other_options
|
491
|
+
scrubber = Loofah::Scrubber.new { |node| node.replace("<h1>#{node.inner_html}</h1>") }
|
492
|
+
html = "<script>hello!</script>"
|
493
|
+
assert_equal "<h1>hello!</h1>", safe_list_sanitize(html, scrubber: scrubber, tags: ["foo"])
|
494
|
+
end
|
331
495
|
|
332
|
-
|
333
|
-
|
334
|
-
assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
|
496
|
+
def test_should_strip_src_attribute_in_img_with_bad_protocols
|
497
|
+
assert_sanitized %(<img src="javascript:bang" title="1">), %(<img title="1">)
|
335
498
|
end
|
336
|
-
end
|
337
499
|
|
338
|
-
|
339
|
-
|
340
|
-
|
500
|
+
def test_should_strip_href_attribute_in_a_with_bad_protocols
|
501
|
+
assert_sanitized %(<a href="javascript:bang" title="1">boo</a>), %(<a title="1">boo</a>)
|
502
|
+
end
|
341
503
|
|
342
|
-
|
343
|
-
|
344
|
-
|
504
|
+
def test_should_block_script_tag
|
505
|
+
assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
|
506
|
+
end
|
345
507
|
|
346
|
-
|
347
|
-
|
348
|
-
%(<IMG SRC=JaVaScRiPt:alert('XSS')>),
|
349
|
-
%(<IMG SRC=javascript:alert("XSS")>),
|
350
|
-
%(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
|
351
|
-
%(<IMG SRC=javascript:alert('XSS')>),
|
352
|
-
%(<IMG SRC=javascript:alert('XSS')>),
|
353
|
-
%(<IMG SRC=javascript:alert('XSS')>),
|
354
|
-
%(<IMG SRC="jav\tascript:alert('XSS');">),
|
355
|
-
%(<IMG SRC="jav	ascript:alert('XSS');">),
|
356
|
-
%(<IMG SRC="jav
ascript:alert('XSS');">),
|
357
|
-
%(<IMG SRC="jav
ascript:alert('XSS');">),
|
358
|
-
%(<IMG SRC="  javascript:alert('XSS');">),
|
359
|
-
%(<IMG SRC="javascript:alert('XSS');">),
|
360
|
-
%(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each do |img_hack|
|
361
|
-
define_method "test_should_not_fall_for_xss_image_hack_#{img_hack}" do
|
362
|
-
assert_sanitized img_hack, "<img>"
|
508
|
+
def test_should_not_fall_for_xss_image_hack_with_uppercase_tags
|
509
|
+
assert_sanitized %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">), %(<img>alert("XSS")">)
|
363
510
|
end
|
364
|
-
end
|
365
511
|
|
366
|
-
|
367
|
-
|
368
|
-
|
512
|
+
[%(<IMG SRC="javascript:alert('XSS');">),
|
513
|
+
%(<IMG SRC=javascript:alert('XSS')>),
|
514
|
+
%(<IMG SRC=JaVaScRiPt:alert('XSS')>),
|
515
|
+
%(<IMG SRC=javascript:alert("XSS")>),
|
516
|
+
%(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
|
517
|
+
%(<IMG SRC=javascript:alert('XSS')>),
|
518
|
+
%(<IMG SRC=javascript:alert('XSS')>),
|
519
|
+
%(<IMG SRC=javascript:alert('XSS')>),
|
520
|
+
%(<IMG SRC="jav\tascript:alert('XSS');">),
|
521
|
+
%(<IMG SRC="jav	ascript:alert('XSS');">),
|
522
|
+
%(<IMG SRC="jav
ascript:alert('XSS');">),
|
523
|
+
%(<IMG SRC="jav
ascript:alert('XSS');">),
|
524
|
+
%(<IMG SRC="  javascript:alert('XSS');">),
|
525
|
+
%(<IMG SRC="javascript:alert('XSS');">),
|
526
|
+
%(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each do |img_hack|
|
527
|
+
define_method "test_should_not_fall_for_xss_image_hack_#{img_hack}" do
|
528
|
+
assert_sanitized img_hack, "<img>"
|
529
|
+
end
|
530
|
+
end
|
369
531
|
|
370
|
-
|
371
|
-
|
372
|
-
|
532
|
+
def test_should_sanitize_tag_broken_up_by_null
|
533
|
+
input = %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>)
|
534
|
+
result = safe_list_sanitize(input)
|
535
|
+
acceptable_results = [
|
536
|
+
# libxml2
|
537
|
+
"",
|
538
|
+
# xerces+neko
|
539
|
+
'alert("XSS")',
|
540
|
+
]
|
541
|
+
|
542
|
+
assert_includes(acceptable_results, result)
|
543
|
+
end
|
373
544
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
end
|
545
|
+
def test_should_sanitize_invalid_script_tag
|
546
|
+
assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
|
547
|
+
end
|
378
548
|
|
379
|
-
|
380
|
-
|
381
|
-
|
549
|
+
def test_should_sanitize_script_tag_with_multiple_open_brackets
|
550
|
+
assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "<alert(\"XSS\");//<"
|
551
|
+
end
|
382
552
|
|
383
|
-
|
384
|
-
|
385
|
-
|
553
|
+
def test_should_sanitize_script_tag_with_multiple_open_brackets_2
|
554
|
+
input = %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a)
|
555
|
+
result = safe_list_sanitize(input)
|
556
|
+
acceptable_results = [
|
557
|
+
# libxml2
|
558
|
+
"",
|
559
|
+
# xerces+neko
|
560
|
+
"<a",
|
561
|
+
]
|
562
|
+
|
563
|
+
assert_includes(acceptable_results, result)
|
564
|
+
end
|
386
565
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
end
|
566
|
+
def test_should_sanitize_unclosed_script
|
567
|
+
assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), ""
|
568
|
+
end
|
391
569
|
|
392
|
-
|
393
|
-
|
394
|
-
|
570
|
+
def test_should_sanitize_half_open_scripts
|
571
|
+
input = %(<IMG SRC="javascript:alert('XSS')")
|
572
|
+
result = safe_list_sanitize(input)
|
573
|
+
acceptable_results = [
|
574
|
+
# libxml2
|
575
|
+
"<img>",
|
576
|
+
# libgumbo
|
577
|
+
"",
|
578
|
+
]
|
579
|
+
|
580
|
+
assert_includes(acceptable_results, result)
|
581
|
+
end
|
395
582
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
end
|
583
|
+
def test_should_not_fall_for_ridiculous_hack
|
584
|
+
img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
|
585
|
+
assert_sanitized img_hack, "<img>"
|
586
|
+
end
|
401
587
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
588
|
+
def test_should_sanitize_attributes
|
589
|
+
input = %(<SPAN title="'><script>alert()</script>">blah</SPAN>)
|
590
|
+
result = safe_list_sanitize(input)
|
591
|
+
acceptable_results = [
|
592
|
+
# libxml2
|
593
|
+
%(<span title="'><script>alert()</script>">blah</span>),
|
594
|
+
# libgumbo
|
595
|
+
# this looks scary, but it's fine. for a more detailed analysis check out:
|
596
|
+
# https://github.com/discourse/discourse/pull/21522#issuecomment-1545697968
|
597
|
+
%(<span title="'><script>alert()</script>">blah</span>)
|
598
|
+
]
|
599
|
+
|
600
|
+
assert_includes(acceptable_results, result)
|
601
|
+
end
|
407
602
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
end
|
603
|
+
def test_should_sanitize_invalid_tag_names
|
604
|
+
assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f")
|
605
|
+
end
|
412
606
|
|
413
|
-
|
414
|
-
|
415
|
-
|
607
|
+
def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags
|
608
|
+
assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>")
|
609
|
+
end
|
416
610
|
|
417
|
-
|
418
|
-
|
419
|
-
|
611
|
+
def test_should_sanitize_invalid_tag_names_in_single_tags
|
612
|
+
input = %(<img/src="http://ha.ckers.org/xss.js"/>)
|
613
|
+
result = safe_list_sanitize(input)
|
614
|
+
acceptable_results = [
|
615
|
+
# libxml2
|
616
|
+
"<img>",
|
617
|
+
# libgumbo
|
618
|
+
%(<img src="http://ha.ckers.org/xss.js">),
|
619
|
+
]
|
620
|
+
|
621
|
+
assert_includes(acceptable_results, result)
|
622
|
+
end
|
420
623
|
|
421
|
-
|
422
|
-
|
423
|
-
|
624
|
+
def test_should_sanitize_img_dynsrc_lowsrc
|
625
|
+
assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img>")
|
626
|
+
end
|
424
627
|
|
425
|
-
|
426
|
-
|
427
|
-
|
628
|
+
def test_should_sanitize_img_vbscript
|
629
|
+
assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), "<img>"
|
630
|
+
end
|
428
631
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
632
|
+
def test_should_sanitize_cdata_section
|
633
|
+
input = "<![CDATA[<span>section</span>]]>"
|
634
|
+
result = safe_list_sanitize(input)
|
635
|
+
acceptable_results = [
|
636
|
+
# libxml2 = 2.9.14
|
637
|
+
%{<![CDATA[<span>section</span>]]>},
|
638
|
+
# other libxml2
|
639
|
+
%{section]]>},
|
640
|
+
# xerces+neko
|
641
|
+
"",
|
642
|
+
]
|
643
|
+
|
644
|
+
assert_includes(acceptable_results, result)
|
438
645
|
end
|
439
|
-
end
|
440
646
|
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
647
|
+
def test_should_sanitize_unterminated_cdata_section
|
648
|
+
input = "<![CDATA[<span>neverending..."
|
649
|
+
result = safe_list_sanitize(input)
|
650
|
+
|
651
|
+
acceptable_results = [
|
652
|
+
# libxml2 = 2.9.14
|
653
|
+
%{<![CDATA[<span>neverending...</span>},
|
654
|
+
# other libxml2
|
655
|
+
%{neverending...},
|
656
|
+
# xerces+neko
|
657
|
+
""
|
658
|
+
]
|
659
|
+
|
660
|
+
assert_includes(acceptable_results, result)
|
448
661
|
end
|
449
|
-
end
|
450
662
|
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
end
|
663
|
+
def test_should_not_mangle_urls_with_ampersand
|
664
|
+
assert_sanitized %{<a href=\"http://www.domain.com?var1=1&var2=2\">my link</a>}
|
665
|
+
end
|
455
666
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
667
|
+
def test_should_sanitize_neverending_attribute
|
668
|
+
# note that assert_dom_equal chokes in this case! so avoid using assert_sanitized
|
669
|
+
assert_equal("<span class=\"\\\"></span>", safe_list_sanitize("<span class=\"\\\">"))
|
670
|
+
end
|
460
671
|
|
461
|
-
|
462
|
-
|
463
|
-
|
672
|
+
[
|
673
|
+
%(<a href="javascript:alert('XSS');">),
|
674
|
+
%(<a href="javascript:alert('XSS');">),
|
675
|
+
%(<a href="javascript:alert('XSS');">),
|
676
|
+
%(<a href="javascript:alert('XSS');">)
|
677
|
+
].each_with_index do |enc_hack, i|
|
678
|
+
define_method "test_x03a_handling_#{i + 1}" do
|
679
|
+
assert_sanitized enc_hack, "<a></a>"
|
680
|
+
end
|
681
|
+
end
|
464
682
|
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
end
|
683
|
+
def test_x03a_legitimate
|
684
|
+
assert_sanitized %(<a href="http://legit">asdf</a>), %(<a href="http://legit">asdf</a>)
|
685
|
+
assert_sanitized %(<a href="http://legit">asdf</a>), %(<a href="http://legit">asdf</a>)
|
686
|
+
end
|
470
687
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
688
|
+
def test_sanitize_ascii_8bit_string
|
689
|
+
safe_list_sanitize("<div><a>hello</a></div>".encode("ASCII-8BIT")).tap do |sanitized|
|
690
|
+
assert_equal "<div><a>hello</a></div>", sanitized
|
691
|
+
assert_equal Encoding::UTF_8, sanitized.encoding
|
692
|
+
end
|
693
|
+
end
|
476
694
|
|
477
|
-
|
478
|
-
|
479
|
-
|
695
|
+
def test_sanitize_data_attributes
|
696
|
+
assert_sanitized %(<a href="/blah" data-method="post">foo</a>), %(<a href="/blah">foo</a>)
|
697
|
+
assert_sanitized %(<a data-remote="true" data-type="script" data-method="get" data-cross-domain="true" href="attack.js">Launch the missiles</a>), %(<a href="attack.js">Launch the missiles</a>)
|
698
|
+
end
|
480
699
|
|
481
|
-
|
482
|
-
|
483
|
-
|
700
|
+
def test_allow_data_attribute_if_requested
|
701
|
+
text = %(<a data-foo="foo">foo</a>)
|
702
|
+
assert_equal %(<a data-foo="foo">foo</a>), safe_list_sanitize(text, attributes: ["data-foo"])
|
703
|
+
end
|
484
704
|
|
485
|
-
|
486
|
-
%
|
487
|
-
|
488
|
-
%(
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
705
|
+
# https://developer.mozilla.org/en-US/docs/Glossary/Void_element
|
706
|
+
VOID_ELEMENTS = %w[area base br col embed hr img input keygen link meta param source track wbr]
|
707
|
+
|
708
|
+
%w(strong em b i p code pre tt samp kbd var sub
|
709
|
+
sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
|
710
|
+
acronym a img blockquote del ins time).each do |tag_name|
|
711
|
+
define_method "test_default_safelist_should_allow_#{tag_name}" do
|
712
|
+
if VOID_ELEMENTS.include?(tag_name)
|
713
|
+
assert_sanitized("<#{tag_name}>")
|
714
|
+
else
|
715
|
+
assert_sanitized("<#{tag_name}>foo</#{tag_name}>")
|
716
|
+
end
|
717
|
+
end
|
493
718
|
end
|
494
|
-
end
|
495
719
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
end
|
720
|
+
def test_datetime_attribute
|
721
|
+
assert_sanitized("<time datetime=\"2023-01-01\">Today</time>")
|
722
|
+
end
|
500
723
|
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
724
|
+
def test_abbr_attribute
|
725
|
+
scope_allowed_tags(%w(table tr th td)) do
|
726
|
+
assert_sanitized(%(<table><tr><td abbr="UK">United Kingdom</td></tr></table>))
|
727
|
+
end
|
505
728
|
end
|
506
|
-
end
|
507
729
|
|
508
|
-
|
509
|
-
|
510
|
-
assert_sanitized %(<a data-remote="true" data-type="script" data-method="get" data-cross-domain="true" href="attack.js">Launch the missiles</a>), %(<a href="attack.js">Launch the missiles</a>)
|
511
|
-
end
|
730
|
+
def test_uri_escaping_of_href_attr_in_a_tag_in_safe_list_sanitizer
|
731
|
+
html = %{<a href='examp<!--" unsafeattr=foo()>-->le.com'>test</a>}
|
512
732
|
|
513
|
-
|
514
|
-
text = %(<a data-foo="foo">foo</a>)
|
515
|
-
assert_equal %(<a data-foo="foo">foo</a>), safe_list_sanitize(text, attributes: ['data-foo'])
|
516
|
-
end
|
733
|
+
text = safe_list_sanitize(html)
|
517
734
|
|
518
|
-
|
519
|
-
|
735
|
+
acceptable_results = [
|
736
|
+
# nokogiri's vendored+patched libxml2 (0002-Update-entities-to-remove-handling-of-ssi.patch)
|
737
|
+
%{<a href="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
738
|
+
# system libxml2
|
739
|
+
%{<a href="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
740
|
+
# xerces+neko
|
741
|
+
%{<a href="examp<!--%22 unsafeattr=foo()>-->le.com">test</a>}
|
742
|
+
]
|
520
743
|
|
521
|
-
|
744
|
+
assert_includes(acceptable_results, text)
|
745
|
+
end
|
522
746
|
|
523
|
-
|
747
|
+
def test_uri_escaping_of_src_attr_in_a_tag_in_safe_list_sanitizer
|
748
|
+
html = %{<a src='examp<!--" unsafeattr=foo()>-->le.com'>test</a>}
|
524
749
|
|
525
|
-
|
526
|
-
# nokogiri w/vendored+patched libxml2
|
527
|
-
%{<a href="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
528
|
-
# nokogiri w/ system libxml2
|
529
|
-
%{<a href="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
530
|
-
]
|
531
|
-
assert_includes(acceptable_results, text)
|
532
|
-
end
|
750
|
+
text = safe_list_sanitize(html)
|
533
751
|
|
534
|
-
|
535
|
-
|
752
|
+
acceptable_results = [
|
753
|
+
# nokogiri's vendored+patched libxml2 (0002-Update-entities-to-remove-handling-of-ssi.patch)
|
754
|
+
%{<a src="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
755
|
+
# system libxml2
|
756
|
+
%{<a src="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
757
|
+
# xerces+neko
|
758
|
+
%{<a src="examp<!--%22 unsafeattr=foo()>-->le.com">test</a>}
|
759
|
+
]
|
536
760
|
|
537
|
-
|
761
|
+
assert_includes(acceptable_results, text)
|
762
|
+
end
|
538
763
|
|
539
|
-
|
764
|
+
def test_uri_escaping_of_name_attr_in_a_tag_in_safe_list_sanitizer
|
765
|
+
html = %{<a name='examp<!--" unsafeattr=foo()>-->le.com'>test</a>}
|
540
766
|
|
541
|
-
|
542
|
-
# nokogiri w/vendored+patched libxml2
|
543
|
-
%{<a src="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
544
|
-
# nokogiri w/system libxml2
|
545
|
-
%{<a src="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
546
|
-
]
|
547
|
-
assert_includes(acceptable_results, text)
|
548
|
-
end
|
767
|
+
text = safe_list_sanitize(html)
|
549
768
|
|
550
|
-
|
551
|
-
|
769
|
+
acceptable_results = [
|
770
|
+
# nokogiri's vendored+patched libxml2 (0002-Update-entities-to-remove-handling-of-ssi.patch)
|
771
|
+
%{<a name="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
772
|
+
# system libxml2
|
773
|
+
%{<a name="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
774
|
+
# xerces+neko
|
775
|
+
%{<a name="examp<!--%22 unsafeattr=foo()>-->le.com">test</a>}
|
776
|
+
]
|
552
777
|
|
553
|
-
|
778
|
+
assert_includes(acceptable_results, text)
|
779
|
+
end
|
554
780
|
|
555
|
-
|
781
|
+
def test_uri_escaping_of_name_action_in_a_tag_in_safe_list_sanitizer
|
782
|
+
html = %{<a action='examp<!--" unsafeattr=foo()>-->le.com'>test</a>}
|
556
783
|
|
557
|
-
|
558
|
-
# nokogiri w/vendored+patched libxml2
|
559
|
-
%{<a name="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
560
|
-
# nokogiri w/system libxml2
|
561
|
-
%{<a name="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
562
|
-
]
|
563
|
-
assert_includes(acceptable_results, text)
|
564
|
-
end
|
784
|
+
text = safe_list_sanitize(html, attributes: ["action"])
|
565
785
|
|
566
|
-
|
567
|
-
|
786
|
+
acceptable_results = [
|
787
|
+
# nokogiri's vendored+patched libxml2 (0002-Update-entities-to-remove-handling-of-ssi.patch)
|
788
|
+
%{<a action="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
789
|
+
# system libxml2
|
790
|
+
%{<a action="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
791
|
+
# xerces+neko
|
792
|
+
%{<a action="examp<!--%22 unsafeattr=foo()>-->le.com">test</a>},
|
793
|
+
]
|
568
794
|
|
569
|
-
|
795
|
+
assert_includes(acceptable_results, text)
|
796
|
+
end
|
570
797
|
|
571
|
-
|
798
|
+
def test_exclude_node_type_processing_instructions
|
799
|
+
input = "<div>text</div><?div content><b>text</b>"
|
800
|
+
result = safe_list_sanitize(input)
|
801
|
+
acceptable_results = [
|
802
|
+
# jruby cyberneko (nokogiri < 1.14.0)
|
803
|
+
"<div>text</div>",
|
804
|
+
# everything else
|
805
|
+
"<div>text</div><b>text</b>",
|
806
|
+
]
|
807
|
+
|
808
|
+
assert_includes(acceptable_results, result)
|
809
|
+
end
|
572
810
|
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
# nokogiri w/system libxml2
|
577
|
-
%{<a action="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
|
578
|
-
]
|
579
|
-
assert_includes(acceptable_results, text)
|
580
|
-
end
|
811
|
+
def test_exclude_node_type_comment
|
812
|
+
assert_equal("<div>text</div><b>text</b>", safe_list_sanitize("<div>text</div><!-- comment --><b>text</b>"))
|
813
|
+
end
|
581
814
|
|
582
|
-
|
583
|
-
|
584
|
-
|
815
|
+
%w[text/plain text/css image/png image/gif image/jpeg].each do |mediatype|
|
816
|
+
define_method "test_mediatype_#{mediatype}_allowed" do
|
817
|
+
input = %Q(<img src="data:#{mediatype};base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">)
|
818
|
+
expected = input
|
819
|
+
actual = safe_list_sanitize(input)
|
820
|
+
assert_equal(expected, actual)
|
821
|
+
|
822
|
+
input = %Q(<img src="DATA:#{mediatype};base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">)
|
823
|
+
expected = input
|
824
|
+
actual = safe_list_sanitize(input)
|
825
|
+
assert_equal(expected, actual)
|
826
|
+
end
|
827
|
+
end
|
585
828
|
|
586
|
-
|
587
|
-
|
588
|
-
|
829
|
+
def test_mediatype_text_html_disallowed
|
830
|
+
input = '<img src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">'
|
831
|
+
expected = "<img>"
|
832
|
+
actual = safe_list_sanitize(input)
|
833
|
+
assert_equal(expected, actual)
|
589
834
|
|
590
|
-
|
591
|
-
|
592
|
-
input = %Q(<img src="data:#{mediatype};base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">)
|
593
|
-
expected = input
|
835
|
+
input = '<img src="DATA:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">'
|
836
|
+
expected = "<img>"
|
594
837
|
actual = safe_list_sanitize(input)
|
595
838
|
assert_equal(expected, actual)
|
839
|
+
end
|
596
840
|
|
597
|
-
|
598
|
-
|
841
|
+
def test_mediatype_image_svg_xml_disallowed
|
842
|
+
input = '<img src="data:image/svg+xml;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">'
|
843
|
+
expected = "<img>"
|
844
|
+
actual = safe_list_sanitize(input)
|
845
|
+
assert_equal(expected, actual)
|
846
|
+
|
847
|
+
input = '<img src="DATA:image/svg+xml;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">'
|
848
|
+
expected = "<img>"
|
599
849
|
actual = safe_list_sanitize(input)
|
600
850
|
assert_equal(expected, actual)
|
601
851
|
end
|
602
|
-
end
|
603
852
|
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
853
|
+
def test_mediatype_other_disallowed
|
854
|
+
input = '<a href="data:foo;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">foo</a>'
|
855
|
+
expected = "<a>foo</a>"
|
856
|
+
actual = safe_list_sanitize(input)
|
857
|
+
assert_equal(expected, actual)
|
609
858
|
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
859
|
+
input = '<a href="DATA:foo;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">foo</a>'
|
860
|
+
expected = "<a>foo</a>"
|
861
|
+
actual = safe_list_sanitize(input)
|
862
|
+
assert_equal(expected, actual)
|
863
|
+
end
|
615
864
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
865
|
+
def test_scrubbing_svg_attr_values_that_allow_ref
|
866
|
+
input = '<div fill="yellow url(http://bad.com/) #fff">hey</div>'
|
867
|
+
expected = '<div fill="yellow #fff">hey</div>'
|
868
|
+
actual = scope_allowed_attributes %w(fill) do
|
869
|
+
safe_list_sanitize(input)
|
870
|
+
end
|
621
871
|
|
622
|
-
|
623
|
-
|
624
|
-
actual = safe_list_sanitize(input)
|
625
|
-
assert_equal(expected, actual)
|
626
|
-
end
|
872
|
+
assert_equal(expected, actual)
|
873
|
+
end
|
627
874
|
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
875
|
+
def test_style_with_css_payload
|
876
|
+
input, tags = "<style>div > span { background: \"red\"; }</style>", ["style"]
|
877
|
+
actual = safe_list_sanitize(input, tags: tags)
|
878
|
+
acceptable_results = [
|
879
|
+
# libxml2
|
880
|
+
"<style>div > span { background: \"red\"; }</style>",
|
881
|
+
# libgumbo
|
882
|
+
"<style>div > span { background: \"red\"; }</style>",
|
883
|
+
]
|
884
|
+
|
885
|
+
assert_includes(acceptable_results, actual)
|
886
|
+
end
|
633
887
|
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
888
|
+
def test_combination_of_select_and_style_with_css_payload
|
889
|
+
input, tags = "<select><style>div > span { background: \"red\"; }</style></select>", ["select", "style"]
|
890
|
+
actual = safe_list_sanitize(input, tags: tags)
|
891
|
+
acceptable_results = [
|
892
|
+
# libxml2
|
893
|
+
"<select><style>div > span { background: \"red\"; }</style></select>",
|
894
|
+
# libgumbo
|
895
|
+
"<select>div > span { background: \"red\"; }</select>",
|
896
|
+
]
|
897
|
+
|
898
|
+
assert_includes(acceptable_results, actual)
|
899
|
+
end
|
639
900
|
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
901
|
+
def test_combination_of_select_and_style_with_script_payload
|
902
|
+
input, tags = "<select><style><script>alert(1)</script></style></select>", ["select", "style"]
|
903
|
+
actual = safe_list_sanitize(input, tags: tags)
|
904
|
+
acceptable_results = [
|
905
|
+
# libxml2
|
906
|
+
"<select><style><script>alert(1)</script></style></select>",
|
907
|
+
# libgumbo
|
908
|
+
"<select>alert(1)</select>",
|
909
|
+
]
|
910
|
+
|
911
|
+
assert_includes(acceptable_results, actual)
|
645
912
|
end
|
646
913
|
|
647
|
-
|
648
|
-
|
914
|
+
def test_combination_of_svg_and_style_with_script_payload
|
915
|
+
input, tags = "<svg><style><script>alert(1)</script></style></svg>", ["svg", "style"]
|
916
|
+
actual = safe_list_sanitize(input, tags: tags)
|
917
|
+
acceptable_results = [
|
918
|
+
# libxml2
|
919
|
+
"<svg><style><script>alert(1)</script></style></svg>",
|
920
|
+
# libgumbo
|
921
|
+
"<svg><style></style></svg>",
|
922
|
+
]
|
923
|
+
|
924
|
+
assert_includes(acceptable_results, actual)
|
925
|
+
end
|
649
926
|
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
927
|
+
def test_combination_of_math_and_style_with_img_payload
|
928
|
+
input, tags = "<math><style><img src=x onerror=alert(1)></style></math>", ["math", "style"]
|
929
|
+
actual = safe_list_sanitize(input, tags: tags)
|
930
|
+
acceptable_results = [
|
931
|
+
# libxml2
|
932
|
+
"<math><style><img src=x onerror=alert(1)></style></math>",
|
933
|
+
# libgumbo
|
934
|
+
"<math><style></style></math>",
|
935
|
+
]
|
936
|
+
|
937
|
+
assert_includes(acceptable_results, actual)
|
938
|
+
end
|
654
939
|
|
655
|
-
|
656
|
-
|
940
|
+
def test_combination_of_math_and_style_with_img_payload_2
|
941
|
+
input, tags = "<math><style><img src=x onerror=alert(1)></style></math>", ["math", "style", "img"]
|
942
|
+
actual = safe_list_sanitize(input, tags: tags)
|
943
|
+
acceptable_results = [
|
944
|
+
# libxml2
|
945
|
+
"<math><style><img src=x onerror=alert(1)></style></math>",
|
946
|
+
# libgumbo
|
947
|
+
"<math><style></style></math><img src=\"x\">",
|
948
|
+
]
|
949
|
+
|
950
|
+
assert_includes(acceptable_results, actual)
|
951
|
+
end
|
657
952
|
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
953
|
+
def test_combination_of_svg_and_style_with_img_payload
|
954
|
+
input, tags = "<svg><style><img src=x onerror=alert(1)></style></svg>", ["svg", "style"]
|
955
|
+
actual = safe_list_sanitize(input, tags: tags)
|
956
|
+
acceptable_results = [
|
957
|
+
# libxml2
|
958
|
+
"<svg><style><img src=x onerror=alert(1)></style></svg>",
|
959
|
+
# libgumbo
|
960
|
+
"<svg><style></style></svg>",
|
961
|
+
]
|
962
|
+
|
963
|
+
assert_includes(acceptable_results, actual)
|
964
|
+
end
|
662
965
|
|
663
|
-
|
664
|
-
|
966
|
+
def test_combination_of_svg_and_style_with_img_payload_2
|
967
|
+
input, tags = "<svg><style><img src=x onerror=alert(1)></style></svg>", ["svg", "style", "img"]
|
968
|
+
actual = safe_list_sanitize(input, tags: tags)
|
969
|
+
acceptable_results = [
|
970
|
+
# libxml2
|
971
|
+
"<svg><style><img src=x onerror=alert(1)></style></svg>",
|
972
|
+
# libgumbo
|
973
|
+
"<svg><style></style></svg><img src=\"x\">",
|
974
|
+
]
|
975
|
+
|
976
|
+
assert_includes(acceptable_results, actual)
|
977
|
+
end
|
665
978
|
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
979
|
+
def test_combination_of_svg_and_style_with_escaped_img_payload
|
980
|
+
# https://hackerone.com/reports/2503220
|
981
|
+
input, tags = "<svg><style><img src onerror=alert(1)>", ["svg", "style"]
|
982
|
+
actual = safe_list_sanitize(input, tags: tags)
|
983
|
+
acceptable_results = [
|
984
|
+
# libxml2
|
985
|
+
"<svg><style>&lt;img src onerror=alert(1)></style></svg>",
|
986
|
+
# libgumbo
|
987
|
+
"<svg><style><img src onerror=alert(1)></style></svg>",
|
988
|
+
]
|
989
|
+
|
990
|
+
assert_includes(acceptable_results, actual)
|
991
|
+
end
|
670
992
|
|
671
|
-
|
672
|
-
|
993
|
+
def test_combination_of_math_and_style_with_escaped_img_payload
|
994
|
+
# https://hackerone.com/reports/2503220
|
995
|
+
input, tags = "<math><style><img src onerror=alert(1)>", ["math", "style"]
|
996
|
+
actual = safe_list_sanitize(input, tags: tags)
|
997
|
+
acceptable_results = [
|
998
|
+
# libxml2
|
999
|
+
"<math><style>&lt;img src onerror=alert(1)></style></math>",
|
1000
|
+
# libgumbo
|
1001
|
+
"<math><style><img src onerror=alert(1)></style></math>",
|
1002
|
+
]
|
1003
|
+
|
1004
|
+
assert_includes(acceptable_results, actual)
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
def test_combination_of_style_and_disallowed_svg_with_script_payload
|
1008
|
+
# https://hackerone.com/reports/2519936
|
1009
|
+
input, tags = "<svg><style><style class='</style><script>alert(1)</script>'>", ["style"]
|
1010
|
+
actual = safe_list_sanitize(input, tags: tags)
|
1011
|
+
acceptable_results = [
|
1012
|
+
# libxml2
|
1013
|
+
"<style><style class='</style>alert(1)'>",
|
1014
|
+
# libgumbo
|
1015
|
+
"",
|
1016
|
+
]
|
1017
|
+
|
1018
|
+
assert_includes(acceptable_results, actual)
|
1019
|
+
end
|
673
1020
|
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
1021
|
+
def test_combination_of_style_and_disallowed_math_with_script_payload
|
1022
|
+
# https://hackerone.com/reports/2519936
|
1023
|
+
input, tags = "<math><style><style class='</style><script>alert(1)</script>'>", ["style"]
|
1024
|
+
actual = safe_list_sanitize(input, tags: tags)
|
1025
|
+
acceptable_results = [
|
1026
|
+
# libxml2
|
1027
|
+
"<style><style class='</style>alert(1)'>",
|
1028
|
+
# libgumbo
|
1029
|
+
"",
|
1030
|
+
]
|
1031
|
+
|
1032
|
+
assert_includes(acceptable_results, actual)
|
1033
|
+
end
|
678
1034
|
|
679
|
-
|
680
|
-
|
1035
|
+
def test_math_with_disallowed_mtext_and_img_payload
|
1036
|
+
# https://hackerone.com/reports/2519941
|
1037
|
+
input, tags = "<math><mtext><table><mglyph><style><img src=: onerror=alert(1)>", ["math", "style"]
|
1038
|
+
actual = safe_list_sanitize(input, tags: tags)
|
1039
|
+
acceptable_results = [
|
1040
|
+
# libxml2
|
1041
|
+
"<math><style><img src=: onerror=alert(1)></style></math>",
|
1042
|
+
# libgumbo
|
1043
|
+
"<math></math>",
|
1044
|
+
]
|
1045
|
+
|
1046
|
+
assert_includes(acceptable_results, actual)
|
1047
|
+
end
|
681
1048
|
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
1049
|
+
def test_should_sanitize_illegal_style_properties
|
1050
|
+
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
|
1051
|
+
expected = %(display:block;width:100%;height:100%;background-color:black;background-x:center;background-y:center;)
|
1052
|
+
assert_equal expected, sanitize_css(raw)
|
1053
|
+
end
|
686
1054
|
|
687
|
-
|
1055
|
+
def test_should_sanitize_with_trailing_space
|
1056
|
+
raw = "display:block; "
|
1057
|
+
expected = "display:block;"
|
1058
|
+
assert_equal expected, sanitize_css(raw)
|
1059
|
+
end
|
688
1060
|
|
689
|
-
|
690
|
-
|
691
|
-
|
1061
|
+
def test_should_sanitize_xul_style_attributes
|
1062
|
+
raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss'))
|
1063
|
+
assert_equal "", sanitize_css(raw)
|
1064
|
+
end
|
692
1065
|
|
693
|
-
|
694
|
-
|
1066
|
+
def test_should_sanitize_div_background_image_unicode_encoded
|
1067
|
+
[
|
1068
|
+
convert_to_css_hex("url(javascript:alert(1))", false),
|
1069
|
+
convert_to_css_hex("url(javascript:alert(1))", true),
|
1070
|
+
convert_to_css_hex("url(https://example.com)", false),
|
1071
|
+
convert_to_css_hex("url(https://example.com)", true),
|
1072
|
+
].each do |propval|
|
1073
|
+
raw = "background-image:" + propval
|
1074
|
+
assert_empty(sanitize_css(raw))
|
1075
|
+
end
|
1076
|
+
end
|
695
1077
|
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
1078
|
+
def test_should_allow_div_background_image_unicode_encoded_safe_functions
|
1079
|
+
[
|
1080
|
+
convert_to_css_hex("rgb(255,0,0)", false),
|
1081
|
+
convert_to_css_hex("rgb(255,0,0)", true),
|
1082
|
+
].each do |propval|
|
1083
|
+
raw = "background-image:" + propval
|
700
1084
|
|
701
|
-
|
1085
|
+
assert_includes(sanitize_css(raw), "background-image")
|
1086
|
+
end
|
1087
|
+
end
|
702
1088
|
|
703
|
-
|
704
|
-
|
705
|
-
|
1089
|
+
def test_should_sanitize_div_style_expression
|
1090
|
+
raw = %(width: expression(alert('XSS'));)
|
1091
|
+
assert_equal "", sanitize_css(raw)
|
1092
|
+
end
|
706
1093
|
|
707
|
-
|
708
|
-
|
1094
|
+
def test_should_sanitize_across_newlines
|
1095
|
+
raw = %(\nwidth:\nexpression(alert('XSS'));\n)
|
1096
|
+
assert_equal "", sanitize_css(raw)
|
1097
|
+
end
|
709
1098
|
|
710
|
-
|
1099
|
+
def test_should_prune_mglyph
|
1100
|
+
# https://hackerone.com/reports/2519936
|
1101
|
+
input = "<math><mtext><table><mglyph><style><img src=: onerror=alert(1)>"
|
1102
|
+
tags = %w(math mtext table mglyph style).freeze
|
711
1103
|
|
712
|
-
|
713
|
-
|
714
|
-
|
1104
|
+
actual = nil
|
1105
|
+
assert_output(nil, /WARNING: 'mglyph' tags cannot be allowed by the PermitScrubber/) do
|
1106
|
+
actual = safe_list_sanitize(input, tags: tags)
|
1107
|
+
end
|
715
1108
|
|
716
|
-
|
717
|
-
|
718
|
-
|
1109
|
+
acceptable_results = [
|
1110
|
+
# libxml2
|
1111
|
+
"<math><mtext><table><style><img src=: onerror=alert(1)></style></table></mtext></math>",
|
1112
|
+
# libgumbo
|
1113
|
+
"<math><mtext><style><img src=: onerror=alert(1)></style><table></table></mtext></math>",
|
1114
|
+
]
|
719
1115
|
|
720
|
-
|
721
|
-
|
722
|
-
end
|
1116
|
+
assert_includes(acceptable_results, actual)
|
1117
|
+
end
|
723
1118
|
|
724
|
-
|
725
|
-
|
726
|
-
|
1119
|
+
def test_should_prune_malignmark
|
1120
|
+
# https://hackerone.com/reports/2519936
|
1121
|
+
input = "<math><mtext><table><malignmark><style><img src=: onerror=alert(1)>"
|
1122
|
+
tags = %w(math mtext table malignmark style).freeze
|
1123
|
+
|
1124
|
+
actual = nil
|
1125
|
+
assert_output(nil, /WARNING: 'malignmark' tags cannot be allowed by the PermitScrubber/) do
|
1126
|
+
actual = safe_list_sanitize(input, tags: tags)
|
1127
|
+
end
|
727
1128
|
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
1129
|
+
acceptable_results = [
|
1130
|
+
# libxml2
|
1131
|
+
"<math><mtext><table><style><img src=: onerror=alert(1)></style></table></mtext></math>",
|
1132
|
+
# libgumbo
|
1133
|
+
"<math><mtext><style><img src=: onerror=alert(1)></style><table></table></mtext></math>",
|
1134
|
+
]
|
1135
|
+
|
1136
|
+
assert_includes(acceptable_results, actual)
|
733
1137
|
end
|
734
|
-
end
|
735
1138
|
|
736
|
-
|
737
|
-
|
738
|
-
|
1139
|
+
def test_should_prune_noscript
|
1140
|
+
# https://hackerone.com/reports/2509647
|
1141
|
+
input = "<div><noscript><p id='</noscript><script>alert(1)</script>'></noscript>"
|
1142
|
+
tags = ["p", "div", "noscript"].freeze
|
739
1143
|
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
ensure
|
745
|
-
Rails::Html::SafeListSanitizer.allowed_tags = old_tags
|
746
|
-
end
|
1144
|
+
actual = nil
|
1145
|
+
assert_output(nil, /WARNING: 'noscript' tags cannot be allowed by the PermitScrubber/) do
|
1146
|
+
actual = safe_list_sanitize(input, tags: tags, attributes: %w(id))
|
1147
|
+
end
|
747
1148
|
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
1149
|
+
acceptable_results = [
|
1150
|
+
# libxml2
|
1151
|
+
"<div><p id=\"</noscript><script>alert(1)</script>\"></p></div>",
|
1152
|
+
# libgumbo
|
1153
|
+
"<div><p id=\"</noscript><script>alert(1)</script>\"></p></div>",
|
1154
|
+
]
|
1155
|
+
|
1156
|
+
assert_includes(acceptable_results, actual)
|
1157
|
+
end
|
755
1158
|
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
1159
|
+
protected
|
1160
|
+
def safe_list_sanitize(input, options = {})
|
1161
|
+
module_under_test::SafeListSanitizer.new.sanitize(input, options)
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
def assert_sanitized(input, expected = nil)
|
1165
|
+
assert_equal((expected || input), safe_list_sanitize(input))
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
def scope_allowed_tags(tags)
|
1169
|
+
old_tags = module_under_test::SafeListSanitizer.allowed_tags
|
1170
|
+
module_under_test::SafeListSanitizer.allowed_tags = tags
|
1171
|
+
yield module_under_test::SafeListSanitizer.new
|
1172
|
+
ensure
|
1173
|
+
module_under_test::SafeListSanitizer.allowed_tags = old_tags
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def scope_allowed_attributes(attributes)
|
1177
|
+
old_attributes = module_under_test::SafeListSanitizer.allowed_attributes
|
1178
|
+
module_under_test::SafeListSanitizer.allowed_attributes = attributes
|
1179
|
+
yield module_under_test::SafeListSanitizer.new
|
1180
|
+
ensure
|
1181
|
+
module_under_test::SafeListSanitizer.allowed_attributes = old_attributes
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
def sanitize_css(input)
|
1185
|
+
module_under_test::SafeListSanitizer.new.sanitize_css(input)
|
763
1186
|
end
|
764
|
-
end.join
|
765
|
-
end
|
766
1187
|
|
767
|
-
|
768
|
-
|
769
|
-
|
1188
|
+
# note that this is used for testing CSS hex encoding: \\[0-9a-f]{1,6}
|
1189
|
+
def convert_to_css_hex(string, escape_parens = false)
|
1190
|
+
string.chars.map do |c|
|
1191
|
+
if !escape_parens && (c == "(" || c == ")")
|
1192
|
+
c
|
1193
|
+
else
|
1194
|
+
format('\00%02X', c.ord)
|
1195
|
+
end
|
1196
|
+
end.join
|
1197
|
+
end
|
770
1198
|
end
|
771
1199
|
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
Nokogiri.method(:uses_libxml?).arity == -1 && Nokogiri.uses_libxml?("= 2.9.14")
|
1200
|
+
class HTML4SafeListSanitizerTest < Minitest::Test
|
1201
|
+
@module_under_test = Rails::HTML4
|
1202
|
+
include SafeListSanitizerTest
|
776
1203
|
end
|
1204
|
+
|
1205
|
+
class HTML5SafeListSanitizerTest < Minitest::Test
|
1206
|
+
@module_under_test = Rails::HTML5
|
1207
|
+
include SafeListSanitizerTest
|
1208
|
+
|
1209
|
+
def test_should_not_be_vulnerable_to_nokogiri_foreign_style_serialization_bug
|
1210
|
+
# https://hackerone.com/reports/2503220
|
1211
|
+
input = "<svg><style><img src onerror=alert(1)>"
|
1212
|
+
result = Rails::HTML5::SafeListSanitizer.new.sanitize(input, tags: ["svg", "style"])
|
1213
|
+
browser = Nokogiri::HTML5::Document.parse(result)
|
1214
|
+
xss = browser.at_xpath("//img/@onerror")
|
1215
|
+
|
1216
|
+
assert_nil(xss)
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
def test_should_not_be_vulnerable_to_ns_confusion_2519936
|
1220
|
+
# https://hackerone.com/reports/2519936
|
1221
|
+
input = "<math><style><style class='</style><script>alert(1)</script>'>"
|
1222
|
+
result = Rails::HTML5::SafeListSanitizer.new.sanitize(input, tags: ["style"])
|
1223
|
+
browser = Nokogiri::HTML5::Document.parse(result)
|
1224
|
+
xss = browser.at_xpath("//script")
|
1225
|
+
|
1226
|
+
assert_nil(xss)
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
def test_should_not_be_vulnerable_to_ns_confusion_2519941
|
1230
|
+
# https://hackerone.com/reports/2519941
|
1231
|
+
input = "<math><mtext><table><mglyph><style><img src=: onerror=alert(1)>"
|
1232
|
+
result = Rails::HTML5::SafeListSanitizer.new.sanitize(input, tags: %w(math style))
|
1233
|
+
browser = Nokogiri::HTML5::Document.parse(result)
|
1234
|
+
xss = browser.at_xpath("//img/@onerror")
|
1235
|
+
|
1236
|
+
assert_nil(xss)
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
def test_should_not_be_vulnerable_to_mglyph_namespace_confusion
|
1240
|
+
# https://hackerone.com/reports/2519936
|
1241
|
+
input = "<math><mtext><table><mglyph><style><img src=: onerror=alert(1)>"
|
1242
|
+
tags = %w(math mtext table mglyph style)
|
1243
|
+
|
1244
|
+
result = nil
|
1245
|
+
assert_output(nil, /WARNING/) do
|
1246
|
+
result = safe_list_sanitize(input, tags: tags)
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
browser = Nokogiri::HTML5::Document.parse(result)
|
1250
|
+
xss = browser.at_xpath("//img/@onerror")
|
1251
|
+
|
1252
|
+
assert_nil(xss)
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
def test_should_not_be_vulnerable_to_malignmark_namespace_confusion
|
1256
|
+
# https://hackerone.com/reports/2519936
|
1257
|
+
input = "<math><mtext><table><malignmark><style><img src=: onerror=alert(1)>"
|
1258
|
+
tags = %w(math mtext table malignmark style)
|
1259
|
+
|
1260
|
+
result = nil
|
1261
|
+
assert_output(nil, /WARNING/) do
|
1262
|
+
result = safe_list_sanitize(input, tags: tags)
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
browser = Nokogiri::HTML5::Document.parse(result)
|
1266
|
+
xss = browser.at_xpath("//img/@onerror")
|
1267
|
+
|
1268
|
+
assert_nil(xss)
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
def test_should_not_be_vulnerable_to_noscript_attacks
|
1272
|
+
# https://hackerone.com/reports/2509647
|
1273
|
+
skip("browser assertion requires parse_noscript_content_as_text") unless Nokogiri::VERSION >= "1.17"
|
1274
|
+
|
1275
|
+
input = '<noscript><p id="</noscript><script>alert(1)</script>"></noscript>'
|
1276
|
+
|
1277
|
+
result = nil
|
1278
|
+
assert_output(nil, /WARNING/) do
|
1279
|
+
result = Rails::HTML5::SafeListSanitizer.new.sanitize(input, tags: %w(p div noscript), attributes: %w(id class style))
|
1280
|
+
end
|
1281
|
+
|
1282
|
+
browser = Nokogiri::HTML5::Document.parse(result, parse_noscript_content_as_text: true)
|
1283
|
+
xss = browser.at_xpath("//script")
|
1284
|
+
|
1285
|
+
assert_nil(xss)
|
1286
|
+
end
|
1287
|
+
end if loofah_html5_support?
|
777
1288
|
end
|