Project

General

Profile

Feature #44585 » 0001-Extract-the-sanitization-rules-common-to-all-text-fo.patch

Go MAEDA, 2026-10-09 07:20

View differences:

lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
20 20
module Redmine
21 21
  module WikiFormatting
22 22
    module CommonMark
23
      # sanitizes rendered HTML using the Sanitize gem
24
      class SanitizationFilter
25
        include Redmine::Helpers::URL
26

  
27
        attr_accessor :allowlist
28

  
29
        LISTS     = Set.new(%w[ul ol].freeze)
30
        LIST_ITEM = 'li'
31

  
32
        # List of table child elements. These must be contained by a <table> element
33
        # or they are not allowed through. Otherwise they can be used to break out
34
        # of places we're using tables to contain formatted user content (like pull
35
        # request review comments).
36
        TABLE_ITEMS = Set.new(%w[tr td th].freeze)
37
        TABLE = 'table'
38
        TABLE_SECTIONS = Set.new(%w[thead tbody tfoot].freeze)
39

  
40
        # The main sanitization allowlist. Only these elements and attributes are
41
        # allowed through by default.
42
        ALLOWLIST = {
43
          :elements => %w[
44
            h1 h2 h3 h4 h5 h6 br b i strong em a pre code img input tt u
45
            div ins del sup sub p ol ul table thead tbody tfoot blockquote
46
            dl dt dd kbd q samp var hr ruby rt rp li tr td th s strike summary
47
            details caption figure figcaption
48
            abbr bdo cite dfn mark small span time wbr
49
          ].freeze,
50
            :remove_contents => ['script'].freeze,
51
            :attributes => {
52
              'a'          => %w[href id name].freeze,
53
              'img'        => %w[src longdesc].freeze,
54
              'code'       => ['class'].freeze,
55
              'div'        => %w[class itemscope itemtype].freeze,
56
              'li'         => %w[id class].freeze,
57
              'input'      => %w[class type].freeze,
58
              'p'          => ['class'].freeze,
59
              'ul'         => ['class'].freeze,
60
              'blockquote' => ['cite'].freeze,
61
              'del'        => ['cite'].freeze,
62
              'ins'        => ['cite'].freeze,
63
              'q'          => ['cite'].freeze,
64
              :all => %w[
65
                abbr accept accept-charset
66
                accesskey action align alt
67
                aria-describedby aria-hidden aria-label aria-labelledby
68
                axis border cellpadding cellspacing char
69
                charoff charset checked
70
                clear cols colspan color
71
                compact coords datetime dir
72
                disabled enctype for frame
73
                headers height hreflang
74
                hspace ismap label lang
75
                maxlength media method
76
                multiple nohref noshade
77
                nowrap open progress prompt readonly rel rev
78
                role rows rowspan rules scope
79
                selected shape size span
80
                start style summary tabindex target
81
                title type usemap valign value
82
                vspace width itemprop
83
              ].freeze
84
            }.freeze,
85
            :protocols => {
86
              'blockquote' => { 'cite' => ['http', 'https', :relative].freeze },
87
              'del'        => { 'cite' => ['http', 'https', :relative].freeze },
88
              'ins'        => { 'cite' => ['http', 'https', :relative].freeze },
89
              'q'          => { 'cite' => ['http', 'https', :relative].freeze },
90
              'img'        => {
91
                'src'      => ['http', 'https', :relative].freeze,
92
                'longdesc' => ['http', 'https', :relative].freeze
93
              }.freeze
94
            },
95
            :transformers => [
96
              # Top-level <li> elements are removed because they can break out of
97
              # containing markup.
98
              lambda { |env|
99
                name = env[:node_name]
100
                node = env[:node]
101
                if name == LIST_ITEM && node.ancestors.none? { |n| LISTS.include?(n.name) }
102
                  node.replace(node.children)
103
                end
104
              },
105

  
106
              # Table child elements that are not contained by a <table> are removed.
107
              lambda { |env|
108
                name = env[:node_name]
109
                node = env[:node]
110
                if (TABLE_SECTIONS.include?(name) || TABLE_ITEMS.include?(name)) && node.ancestors.none? { |n| n.name == TABLE }
111
                  node.replace(node.children)
112
                end
113
              }
114
            ].freeze,
115
            :css => {
116
              :properties => %w[
117
                color background-color
118
                width min-width max-width
119
                height min-height max-height
120
                padding padding-left padding-right padding-top padding-bottom
121
                margin margin-left margin-right margin-top margin-bottom
122
                border border-left border-right border-top border-bottom border-radius border-style border-collapse border-spacing
123
                font font-style font-variant font-weight font-stretch font-size line-height font-family
124
                text-align text-decoration
125
                float
126
              ].freeze
127
            }
128
        }.freeze
129

  
130
        RELAXED_PROTOCOL_ATTRS = {
131
          "a" => %w(href).freeze,
132
        }.freeze
133

  
134
        def initialize
135
          @allowlist = default_allowlist
136
          add_transformers
137
        end
23
      # Sanitizes the HTML generated by the CommonMark formatter
24
      class SanitizationFilter < Redmine::WikiFormatting::SanitizationFilter
25
        private
138 26

  
139
        def call(doc)
140
          # Sanitize is applied to the whole document, so the API is different from loofeh's scrubber.
141
          Sanitize.clean_node!(doc, allowlist)
27
        def default_allowlist
28
          allowlist = super
29
          allowlist[:elements] += %w[input]
30
          allowlist[:attributes].merge!(
31
            'a'     => %w[href id name],
32
            'code'  => %w[class],
33
            'div'   => %w[class itemscope itemtype],
34
            'li'    => %w[id class],
35
            'input' => %w[class type],
36
            'p'     => %w[class],
37
            'ul'    => %w[class]
38
          )
39
          allowlist
142 40
        end
143 41

  
144
        private
145

  
146 42
        def add_transformers
43
          super
44

  
147 45
          # allow class on code tags (this holds the language info from fenced
148 46
          # code bocks and has the format language-foo)
149 47
          allowlist[:transformers].push lambda {|env|
......
227 125

  
228 126
            node.remove_attribute("class")
229 127
          }
230

  
231
          # https://github.com/rgrove/sanitize/issues/209
232
          allowlist[:transformers].push lambda {|env|
233
            node = env[:node]
234
            return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
235

  
236
            name = env[:node_name]
237
            return unless RELAXED_PROTOCOL_ATTRS.include?(name)
238

  
239
            RELAXED_PROTOCOL_ATTRS[name].each do |attr|
240
              next unless node.has_attribute?(attr)
241

  
242
              node[attr] = node[attr].strip
243
              unless !node[attr].empty? && uri_with_link_safe_scheme?(node[attr])
244
                node.remove_attribute(attr)
245
              end
246
            end
247
          }
248
        end
249

  
250
        # The allowlist to use when sanitizing. This can be passed in the context
251
        # hash to the filter but defaults to ALLOWLIST constant value above.
252
        def default_allowlist
253
          ALLOWLIST.deep_dup
254 128
        end
255 129
      end
256 130
    end
lib/redmine/wiki_formatting/sanitization_filter.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module Redmine
21
  module WikiFormatting
22
    # Sanitizes rendered HTML using the Sanitize gem. The allowlist only covers
23
    # what is common to all formatters; classes, ids and elements specific to a
24
    # formatter are allowed by its own subclass.
25
    class SanitizationFilter
26
      include Redmine::Helpers::URL
27

  
28
      attr_accessor :allowlist
29

  
30
      LISTS     = Set.new(%w[ul ol].freeze)
31
      LIST_ITEM = 'li'
32

  
33
      # List of table child elements. These must be contained by a <table> element
34
      # or they are not allowed through. Otherwise they can be used to break out
35
      # of places we're using tables to contain formatted user content (like pull
36
      # request review comments).
37
      TABLE_ITEMS = Set.new(%w[tr td th].freeze)
38
      TABLE = 'table'
39
      TABLE_SECTIONS = Set.new(%w[thead tbody tfoot].freeze)
40

  
41
      # The main sanitization allowlist. Only these elements and attributes are
42
      # allowed through by default.
43
      ALLOWLIST = {
44
        :elements => %w[
45
          h1 h2 h3 h4 h5 h6 br b i strong em a pre code img tt u
46
          div ins del sup sub p ol ul table thead tbody tfoot blockquote
47
          dl dt dd kbd q samp var hr ruby rt rp li tr td th s strike summary
48
          details caption figure figcaption
49
          abbr bdo cite dfn mark small span time wbr
50
        ].freeze,
51
          :remove_contents => ['script'].freeze,
52
          :attributes => {
53
            'a'          => %w[href name].freeze,
54
            'img'        => %w[src longdesc].freeze,
55
            'div'        => %w[itemscope itemtype].freeze,
56
            'blockquote' => ['cite'].freeze,
57
            'del'        => ['cite'].freeze,
58
            'ins'        => ['cite'].freeze,
59
            'q'          => ['cite'].freeze,
60
            :all => %w[
61
              abbr accept accept-charset
62
              accesskey action align alt
63
              aria-describedby aria-hidden aria-label aria-labelledby
64
              axis border cellpadding cellspacing char
65
              charoff charset checked
66
              clear cols colspan color
67
              compact coords datetime dir
68
              disabled enctype for frame
69
              headers height hreflang
70
              hspace ismap label lang
71
              maxlength media method
72
              multiple nohref noshade
73
              nowrap open progress prompt readonly rel rev
74
              role rows rowspan rules scope
75
              selected shape size span
76
              start style summary tabindex target
77
              title type usemap valign value
78
              vspace width itemprop
79
            ].freeze
80
          }.freeze,
81
          :protocols => {
82
            'blockquote' => { 'cite' => ['http', 'https', :relative].freeze },
83
            'del'        => { 'cite' => ['http', 'https', :relative].freeze },
84
            'ins'        => { 'cite' => ['http', 'https', :relative].freeze },
85
            'q'          => { 'cite' => ['http', 'https', :relative].freeze },
86
            'img'        => {
87
              'src'      => ['http', 'https', :relative].freeze,
88
              'longdesc' => ['http', 'https', :relative].freeze
89
            }.freeze
90
          },
91
          :transformers => [
92
            # Top-level <li> elements are removed because they can break out of
93
            # containing markup.
94
            lambda { |env|
95
              name = env[:node_name]
96
              node = env[:node]
97
              if name == LIST_ITEM && node.ancestors.none? { |n| LISTS.include?(n.name) }
98
                node.replace(node.children)
99
              end
100
            },
101

  
102
            # Table child elements that are not contained by a <table> are removed.
103
            lambda { |env|
104
              name = env[:node_name]
105
              node = env[:node]
106
              if (TABLE_SECTIONS.include?(name) || TABLE_ITEMS.include?(name)) && node.ancestors.none? { |n| n.name == TABLE }
107
                node.replace(node.children)
108
              end
109
            }
110
          ].freeze,
111
          :css => {
112
            :properties => %w[
113
              color background-color
114
              width min-width max-width
115
              height min-height max-height
116
              padding padding-left padding-right padding-top padding-bottom
117
              margin margin-left margin-right margin-top margin-bottom
118
              border border-left border-right border-top border-bottom border-radius border-style border-collapse border-spacing
119
              font font-style font-variant font-weight font-stretch font-size line-height font-family
120
              text-align text-decoration
121
              float
122
            ].freeze
123
          }
124
      }.freeze
125

  
126
      RELAXED_PROTOCOL_ATTRS = {
127
        "a" => %w(href).freeze,
128
      }.freeze
129

  
130
      def initialize
131
        @allowlist = default_allowlist
132
        add_transformers
133
      end
134

  
135
      def call(doc)
136
        # Sanitize is applied to the whole document, so the API is different from loofeh's scrubber.
137
        Sanitize.clean_node!(doc, allowlist)
138
      end
139

  
140
      private
141

  
142
      def add_transformers
143
        # https://github.com/rgrove/sanitize/issues/209
144
        allowlist[:transformers].push lambda {|env|
145
          node = env[:node]
146
          return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
147

  
148
          name = env[:node_name]
149
          return unless RELAXED_PROTOCOL_ATTRS.include?(name)
150

  
151
          RELAXED_PROTOCOL_ATTRS[name].each do |attr|
152
            next unless node.has_attribute?(attr)
153

  
154
            node[attr] = node[attr].strip
155
            unless !node[attr].empty? && uri_with_link_safe_scheme?(node[attr])
156
              node.remove_attribute(attr)
157
            end
158
          end
159
        }
160
      end
161

  
162
      # The allowlist to use when sanitizing. This can be passed in the context
163
      # hash to the filter but defaults to ALLOWLIST constant value above.
164
      def default_allowlist
165
        ALLOWLIST.deep_dup
166
      end
167
    end
168
  end
169
end
(1-1/3)