rails-html-sanitizer 1.3.0 → 1.4.3

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.

Potentially problematic release.


This version of rails-html-sanitizer might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8eba1aac52c80be280f186c5d378150709b7d4cd2a5d5b2367e6d2c036648d52
4
- data.tar.gz: 96408eae2efee778a704f7caf246b64868a63bfdbbb81905b294bcca731a9289
3
+ metadata.gz: 2f00d9f256478eb753c8d211c3b25efa4204bbdbc9c5abf0415413c811a2e404
4
+ data.tar.gz: 65d3871aa798dfbbfb1138b666d475b590e347cdb66614d6d39b72ad3531c742
5
5
  SHA512:
6
- metadata.gz: c4209cebc841299143a466143f4b776461fc1cc8bba112dc603e86835b68ee44a800566f64224b27f5a45d164d0b004049b228dc405c3de59068800ec7a5d564
7
- data.tar.gz: c899472b8dffe9f9fd4d15ae4739f07a775d74b9ed14143beb688bb546b6a82ec469add036747b81aff33510e6e241379e21458cb39d9b2a8e797824066e24e5
6
+ metadata.gz: e6e31eaa72b1a2e8356aae50600ac784f85a80828cbc49ce8061384ecd3f21a1d8eaee69845dc08537c5102728c3cc41a72cb3ed8b9789c4921038398afa61e2
7
+ data.tar.gz: 6b14a49842eaf4c3e0fbae5acd28fdf32a5deb6cd42f769aada848226847180c4d3a67a9dcbc439e1a4855699b0ea694cb4c7b6ee173391ac841bd334ae44b6f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,46 @@
1
+ ## 1.4.3 / 2022-06-09
2
+
3
+ * Address a possible XSS vulnerability with certain configurations of Rails::Html::Sanitizer.
4
+
5
+ Prevent the combination of `select` and `style` as allowed tags in SafeListSanitizer.
6
+
7
+ Fixes CVE-2022-32209
8
+
9
+ *Mike Dalessio*
10
+
11
+
12
+ ## 1.4.2 / 2021-08-23
13
+
14
+ * Slightly improve performance.
15
+
16
+ Assuming elements are more common than comments, make one less method call per node.
17
+
18
+ *Mike Dalessio*
19
+
20
+ ## 1.4.1 / 2021-08-18
21
+
22
+ * Fix regression in v1.4.0 that did not pass comment nodes to the scrubber.
23
+
24
+ Some scrubbers will want to override the default behavior and allow comments, but v1.4.0 only
25
+ passed through elements to the scrubber's `keep_node?` method.
26
+
27
+ This change once again allows the scrubber to make the decision on comment nodes, but still skips
28
+ other non-elements like processing instructions (see #115).
29
+
30
+ *Mike Dalessio*
31
+
32
+ ## 1.4.0 / 2021-08-18
33
+
34
+ * Processing Instructions are no longer allowed by Rails::Html::PermitScrubber
35
+
36
+ Previously, a PI with a name (or "target") matching an allowed tag name was not scrubbed. There
37
+ are no known security issues associated with these PIs, but similar to comments it's preferred to
38
+ omit these nodes when possible from sanitized output.
39
+
40
+ Fixes #115.
41
+
42
+ *Mike Dalessio*
43
+
1
44
  ## 1.3.0
2
45
 
3
46
  * Address deprecations in Loofah 2.3.0.
data/README.md CHANGED
@@ -81,8 +81,10 @@ html_fragment.to_s # => "<a></a>"
81
81
  #### `Rails::Html::TargetScrubber`
82
82
 
83
83
  Where `PermitScrubber` picks out tags and attributes to permit in sanitization,
84
- `Rails::Html::TargetScrubber` targets them for removal.
84
+ `Rails::Html::TargetScrubber` targets them for removal. See https://github.com/flavorjones/loofah/blob/main/lib/loofah/html5/safelist.rb for the tag list.
85
85
 
86
+ **Note:** by default, it will scrub anything that is not part of the permitted tags from
87
+ loofah `HTML5::Scrub.allowed_element?`.
86
88
 
87
89
  ```ruby
88
90
  scrubber = Rails::Html::TargetScrubber.new
@@ -1,7 +1,7 @@
1
1
  module Rails
2
2
  module Html
3
3
  class Sanitizer
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.3"
5
5
  end
6
6
  end
7
7
  end
@@ -141,8 +141,25 @@ module Rails
141
141
 
142
142
  private
143
143
 
144
+ def loofah_using_html5?
145
+ # future-proofing, see https://github.com/flavorjones/loofah/pull/239
146
+ Loofah.respond_to?(:html5_mode?) && Loofah.html5_mode?
147
+ end
148
+
149
+ def remove_safelist_tag_combinations(tags)
150
+ if !loofah_using_html5? && tags.include?("select") && tags.include?("style")
151
+ warn("WARNING: #{self.class}: removing 'style' from safelist, should not be combined with 'select'")
152
+ tags.delete("style")
153
+ end
154
+ tags
155
+ end
156
+
144
157
  def allowed_tags(options)
145
- options[:tags] || self.class.allowed_tags
158
+ if options[:tags]
159
+ remove_safelist_tag_combinations(options[:tags])
160
+ else
161
+ self.class.allowed_tags
162
+ end
146
163
  end
147
164
 
148
165
  def allowed_attributes(options)
@@ -68,7 +68,7 @@ module Rails
68
68
  end
69
69
  return CONTINUE if skip_node?(node)
70
70
 
71
- unless keep_node?(node)
71
+ unless (node.element? || node.comment?) && keep_node?(node)
72
72
  return STOP if scrub_node(node) == STOP
73
73
  end
74
74
 
@@ -2,6 +2,8 @@ require "minitest/autorun"
2
2
  require "rails-html-sanitizer"
3
3
  require "rails/dom/testing/assertions/dom_assertions"
4
4
 
5
+ puts Nokogiri::VERSION_INFO
6
+
5
7
  class SanitizersTest < Minitest::Test
6
8
  include Rails::Dom::Testing::Assertions::DomAssertions
7
9
 
@@ -12,13 +14,11 @@ class SanitizersTest < Minitest::Test
12
14
  end
13
15
 
14
16
  def test_sanitize_nested_script
15
- sanitizer = Rails::Html::SafeListSanitizer.new
16
- assert_equal '&lt;script&gt;alert("XSS");&lt;/script&gt;', sanitizer.sanitize('<script><script></script>alert("XSS");<script><</script>/</script><script>script></script>', tags: %w(em))
17
+ assert_equal '&lt;script&gt;alert("XSS");&lt;/script&gt;', safe_list_sanitize('<script><script></script>alert("XSS");<script><</script>/</script><script>script></script>', tags: %w(em))
17
18
  end
18
19
 
19
20
  def test_sanitize_nested_script_in_style
20
- sanitizer = Rails::Html::SafeListSanitizer.new
21
- assert_equal '&lt;script&gt;alert("XSS");&lt;/script&gt;', sanitizer.sanitize('<style><script></style>alert("XSS");<style><</style>/</style><style>script></style>', tags: %w(em))
21
+ assert_equal '&lt;script&gt;alert("XSS");&lt;/script&gt;', safe_list_sanitize('<style><script></style>alert("XSS");<style><</style>/</style><style>script></style>', tags: %w(em))
22
22
  end
23
23
 
24
24
  class XpathRemovalTestSanitizer < Rails::Html::Sanitizer
@@ -54,7 +54,8 @@ class SanitizersTest < Minitest::Test
54
54
 
55
55
  def test_strip_tags_with_quote
56
56
  input = '<" <img src="trollface.gif" onload="alert(1)"> hi'
57
- assert_equal ' hi', full_sanitize(input)
57
+ expected = libxml_2_9_14_recovery? ? %{&lt;" hi} : %{ hi}
58
+ assert_equal(expected, full_sanitize(input))
58
59
  end
59
60
 
60
61
  def test_strip_invalid_html
@@ -75,15 +76,21 @@ class SanitizersTest < Minitest::Test
75
76
  end
76
77
 
77
78
  def test_remove_unclosed_tags
78
- assert_equal "This is ", full_sanitize("This is <-- not\n a comment here.")
79
+ input = "This is <-- not\n a comment here."
80
+ expected = libxml_2_9_14_recovery? ? %{This is &lt;-- not\n a comment here.} : %{This is }
81
+ assert_equal(expected, full_sanitize(input))
79
82
  end
80
83
 
81
84
  def test_strip_cdata
82
- assert_equal "This has a ]]&gt; here.", full_sanitize("This has a <![CDATA[<section>]]> here.")
85
+ input = "This has a <![CDATA[<section>]]> here."
86
+ expected = libxml_2_9_14_recovery? ? %{This has a &lt;![CDATA[]]&gt; here.} : %{This has a ]]&gt; here.}
87
+ assert_equal(expected, full_sanitize(input))
83
88
  end
84
89
 
85
90
  def test_strip_unclosed_cdata
86
- assert_equal "This has an unclosed ]] here...", full_sanitize("This has an unclosed <![CDATA[<section>]] here...")
91
+ input = "This has an unclosed <![CDATA[<section>]] here..."
92
+ expected = libxml_2_9_14_recovery? ? %{This has an unclosed &lt;![CDATA[]] here...} : %{This has an unclosed ]] here...}
93
+ assert_equal(expected, full_sanitize(input))
87
94
  end
88
95
 
89
96
  def test_strip_blank_string
@@ -93,7 +100,7 @@ class SanitizersTest < Minitest::Test
93
100
  end
94
101
 
95
102
  def test_strip_tags_with_plaintext
96
- assert_equal "Dont touch me", full_sanitize("Dont touch me")
103
+ assert_equal "Don't touch me", full_sanitize("Don't touch me")
97
104
  end
98
105
 
99
106
  def test_strip_tags_with_tags
@@ -135,7 +142,7 @@ class SanitizersTest < Minitest::Test
135
142
  end
136
143
 
137
144
  def test_strip_links_with_plaintext
138
- assert_equal "Dont touch me", link_sanitize("Dont touch me")
145
+ assert_equal "Don't touch me", link_sanitize("Don't touch me")
139
146
  end
140
147
 
141
148
  def test_strip_links_with_line_feed_and_uppercase_tag
@@ -271,7 +278,8 @@ class SanitizersTest < Minitest::Test
271
278
 
272
279
  def test_scrub_style_if_style_attribute_option_is_passed
273
280
  input = '<p style="color: #000; background-image: url(http://www.ragingplatypus.com/i/cam-full.jpg);"></p>'
274
- assert_equal '<p style="color: #000;"></p>', safe_list_sanitize(input, attributes: %w(style))
281
+ actual = safe_list_sanitize(input, attributes: %w(style))
282
+ assert_includes(['<p style="color: #000;"></p>', '<p style="color:#000;"></p>'], actual)
275
283
  end
276
284
 
277
285
  def test_should_raise_argument_error_if_tags_is_not_enumerable
@@ -413,8 +421,25 @@ class SanitizersTest < Minitest::Test
413
421
  end
414
422
 
415
423
  def test_should_sanitize_div_background_image_unicode_encoded
416
- raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029)
417
- assert_equal '', sanitize_css(raw)
424
+ [
425
+ convert_to_css_hex("url(javascript:alert(1))", false),
426
+ convert_to_css_hex("url(javascript:alert(1))", true),
427
+ convert_to_css_hex("url(https://example.com)", false),
428
+ convert_to_css_hex("url(https://example.com)", true),
429
+ ].each do |propval|
430
+ raw = "background-image:" + propval
431
+ assert_empty(sanitize_css(raw))
432
+ end
433
+ end
434
+
435
+ def test_should_allow_div_background_image_unicode_encoded_safe_functions
436
+ [
437
+ convert_to_css_hex("rgb(255,0,0)", false),
438
+ convert_to_css_hex("rgb(255,0,0)", true),
439
+ ].each do |propval|
440
+ raw = "background-image:" + propval
441
+ assert_includes(sanitize_css(raw), "background-image")
442
+ end
418
443
  end
419
444
 
420
445
  def test_should_sanitize_div_style_expression
@@ -432,11 +457,15 @@ class SanitizersTest < Minitest::Test
432
457
  end
433
458
 
434
459
  def test_should_sanitize_cdata_section
435
- assert_sanitized "<![CDATA[<span>section</span>]]>", "section]]&gt;"
460
+ input = "<![CDATA[<span>section</span>]]>"
461
+ expected = libxml_2_9_14_recovery? ? %{&lt;![CDATA[<span>section</span>]]&gt;} : %{section]]&gt;}
462
+ assert_sanitized(input, expected)
436
463
  end
437
464
 
438
465
  def test_should_sanitize_unterminated_cdata_section
439
- assert_sanitized "<![CDATA[<span>neverending...", "neverending..."
466
+ input = "<![CDATA[<span>neverending..."
467
+ expected = libxml_2_9_14_recovery? ? %{&lt;![CDATA[<span>neverending...</span>} : %{neverending...}
468
+ assert_sanitized(input, expected)
440
469
  end
441
470
 
442
471
  def test_should_not_mangle_urls_with_ampersand
@@ -487,7 +516,13 @@ class SanitizersTest < Minitest::Test
487
516
 
488
517
  text = safe_list_sanitize(html)
489
518
 
490
- assert_equal %{<a href=\"examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com\">test</a>}, text
519
+ acceptable_results = [
520
+ # nokogiri w/vendored+patched libxml2
521
+ %{<a href="examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com">test</a>},
522
+ # nokogiri w/ system libxml2
523
+ %{<a href="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
524
+ ]
525
+ assert_includes(acceptable_results, text)
491
526
  end
492
527
 
493
528
  def test_uri_escaping_of_src_attr_in_a_tag_in_safe_list_sanitizer
@@ -497,7 +532,13 @@ class SanitizersTest < Minitest::Test
497
532
 
498
533
  text = safe_list_sanitize(html)
499
534
 
500
- assert_equal %{<a src=\"examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com\">test</a>}, text
535
+ acceptable_results = [
536
+ # nokogiri w/vendored+patched libxml2
537
+ %{<a src="examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com">test</a>},
538
+ # nokogiri w/system libxml2
539
+ %{<a src="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
540
+ ]
541
+ assert_includes(acceptable_results, text)
501
542
  end
502
543
 
503
544
  def test_uri_escaping_of_name_attr_in_a_tag_in_safe_list_sanitizer
@@ -507,7 +548,13 @@ class SanitizersTest < Minitest::Test
507
548
 
508
549
  text = safe_list_sanitize(html)
509
550
 
510
- assert_equal %{<a name=\"examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com\">test</a>}, text
551
+ acceptable_results = [
552
+ # nokogiri w/vendored+patched libxml2
553
+ %{<a name="examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com">test</a>},
554
+ # nokogiri w/system libxml2
555
+ %{<a name="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
556
+ ]
557
+ assert_includes(acceptable_results, text)
511
558
  end
512
559
 
513
560
  def test_uri_escaping_of_name_action_in_a_tag_in_safe_list_sanitizer
@@ -517,7 +564,40 @@ class SanitizersTest < Minitest::Test
517
564
 
518
565
  text = safe_list_sanitize(html, attributes: ['action'])
519
566
 
520
- assert_equal %{<a action=\"examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com\">test</a>}, text
567
+ acceptable_results = [
568
+ # nokogiri w/vendored+patched libxml2
569
+ %{<a action="examp&lt;!--%22%20unsafeattr=foo()&gt;--&gt;le.com">test</a>},
570
+ # nokogiri w/system libxml2
571
+ %{<a action="examp<!--%22%20unsafeattr=foo()>-->le.com">test</a>},
572
+ ]
573
+ assert_includes(acceptable_results, text)
574
+ end
575
+
576
+ def test_exclude_node_type_processing_instructions
577
+ assert_equal("<div>text</div><b>text</b>", safe_list_sanitize("<div>text</div><?div content><b>text</b>"))
578
+ end
579
+
580
+ def test_exclude_node_type_comment
581
+ assert_equal("<div>text</div><b>text</b>", safe_list_sanitize("<div>text</div><!-- comment --><b>text</b>"))
582
+ end
583
+
584
+ def test_disallow_the_dangerous_safelist_combination_of_select_and_style
585
+ input = "<select><style><script>alert(1)</script></style></select>"
586
+ tags = ["select", "style"]
587
+ warning = /WARNING: Rails::Html::SafeListSanitizer: removing 'style' from safelist/
588
+ sanitized = nil
589
+ invocation = Proc.new { sanitized = safe_list_sanitize(input, tags: tags) }
590
+
591
+ if html5_mode?
592
+ # if Loofah is using an HTML5 parser,
593
+ # then "style" should be removed by the parser as an invalid child of "select"
594
+ assert_silent(&invocation)
595
+ else
596
+ # if Loofah is using an HTML4 parser,
597
+ # then SafeListSanitizer should remove "style" from the safelist
598
+ assert_output(nil, warning, &invocation)
599
+ end
600
+ refute_includes(sanitized, "style")
521
601
  end
522
602
 
523
603
  protected
@@ -565,4 +645,23 @@ protected
565
645
  ensure
566
646
  Rails::Html::SafeListSanitizer.allowed_attributes = old_attributes
567
647
  end
648
+
649
+ # note that this is used for testing CSS hex encoding: \\[0-9a-f]{1,6}
650
+ def convert_to_css_hex(string, escape_parens=false)
651
+ string.chars.map do |c|
652
+ if !escape_parens && (c == "(" || c == ")")
653
+ c
654
+ else
655
+ format('\00%02X', c.ord)
656
+ end
657
+ end.join
658
+ end
659
+
660
+ def libxml_2_9_14_recovery?
661
+ Nokogiri.method(:uses_libxml?).arity == -1 && Nokogiri.uses_libxml?(">= 2.9.14")
662
+ end
663
+
664
+ def html5_mode?
665
+ ::Loofah.respond_to?(:html5_mode?) && ::Loofah.html5_mode?
666
+ end
568
667
  end
@@ -41,6 +41,16 @@ class PermitScrubberTest < ScrubberTest
41
41
  assert_scrubbed '<tag>hello</tag>', 'hello'
42
42
  end
43
43
 
44
+ def test_default_scrub_removes_comments
45
+ assert_scrubbed('<div>one</div><!-- two --><span>three</span>',
46
+ '<div>one</div><span>three</span>')
47
+ end
48
+
49
+ def test_default_scrub_removes_processing_instructions
50
+ assert_scrubbed('<div>one</div><?div two><span>three</span>',
51
+ '<div>one</div><span>three</span>')
52
+ end
53
+
44
54
  def test_default_attributes_removal_behavior
45
55
  assert_scrubbed '<p cooler="hello">hello</p>', '<p>hello</p>'
46
56
  end
@@ -56,6 +66,12 @@ class PermitScrubberTest < ScrubberTest
56
66
  assert_scrubbed html, '<tag>leave me now</tag>'
57
67
  end
58
68
 
69
+ def test_leaves_comments_when_supplied_as_tag
70
+ @scrubber.tags = %w(div comment)
71
+ assert_scrubbed('<div>one</div><!-- two --><span>three</span>',
72
+ '<div>one</div><!-- two -->three')
73
+ end
74
+
59
75
  def test_leaves_only_supplied_tags_nested
60
76
  html = '<tag>leave <em>me <span>now</span></em></tag>'
61
77
  @scrubber.tags = %w(tag)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-html-sanitizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Mendonça França
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-10-06 00:00:00.000000000 Z
12
+ date: 2022-06-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: loofah
@@ -101,7 +101,11 @@ files:
101
101
  homepage: https://github.com/rails/rails-html-sanitizer
102
102
  licenses:
103
103
  - MIT
104
- metadata: {}
104
+ metadata:
105
+ bug_tracker_uri: https://github.com/rails/rails-html-sanitizer/issues
106
+ changelog_uri: https://github.com/rails/rails-html-sanitizer/blob/v1.4.3/CHANGELOG.md
107
+ documentation_uri: https://www.rubydoc.info/gems/rails-html-sanitizer/1.4.3
108
+ source_code_uri: https://github.com/rails/rails-html-sanitizer/tree/v1.4.3
105
109
  post_install_message:
106
110
  rdoc_options: []
107
111
  require_paths:
@@ -117,10 +121,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
121
  - !ruby/object:Gem::Version
118
122
  version: '0'
119
123
  requirements: []
120
- rubygems_version: 3.0.3
124
+ rubygems_version: 3.3.5
121
125
  signing_key:
122
126
  specification_version: 4
123
127
  summary: This gem is responsible to sanitize HTML fragments in Rails applications.
124
128
  test_files:
125
- - test/scrubbers_test.rb
126
129
  - test/sanitizer_test.rb
130
+ - test/scrubbers_test.rb