lib/sanitize/css.rb in sanitize-6.1.3 vs lib/sanitize/css.rb in sanitize-7.0.0

- old
+ new

@@ -1,381 +1,381 @@ -# encoding: utf-8 +# frozen_string_literal: true -require 'crass' -require 'set' +require "crass" +require "set" -class Sanitize; class CSS - attr_reader :config +class Sanitize + class CSS + attr_reader :config - # -- Class Methods ----------------------------------------------------------- + # -- Class Methods --------------------------------------------------------- - # Sanitizes inline CSS style properties. - # - # This is most useful for sanitizing non-stylesheet fragments of CSS like you - # would find in the `style` attribute of an HTML element. To sanitize a full - # CSS stylesheet, use {.stylesheet}. - # - # @example - # Sanitize::CSS.properties("background: url(foo.png); color: #fff;") - # - # @return [String] Sanitized CSS properties. - def self.properties(css, config = {}) - self.new(config).properties(css) - end + # Sanitizes inline CSS style properties. + # + # This is most useful for sanitizing non-stylesheet fragments of CSS like + # you would find in the `style` attribute of an HTML element. To sanitize a + # full CSS stylesheet, use {.stylesheet}. + # + # @example + # Sanitize::CSS.properties("background: url(foo.png); color: #fff;") + # + # @return [String] Sanitized CSS properties. + def self.properties(css, config = {}) + new(config).properties(css) + end - # Sanitizes a full CSS stylesheet. - # - # A stylesheet may include selectors, at-rules, and comments. To sanitize only - # inline style properties such as the contents of an HTML `style` attribute, - # use {.properties}. - # - # @example - # css = %[ - # .foo { - # background: url(foo.png); - # color: #fff; - # } - # - # #bar { - # font: 42pt 'Comic Sans MS'; - # } - # ] - # - # Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED) - # - # @return [String] Sanitized CSS stylesheet. - def self.stylesheet(css, config = {}) - self.new(config).stylesheet(css) - end + # Sanitizes a full CSS stylesheet. + # + # A stylesheet may include selectors, at-rules, and comments. To sanitize + # only inline style properties such as the contents of an HTML `style` + # attribute, use {.properties}. + # + # @example + # css = %[ + # .foo { + # background: url(foo.png); + # color: #fff; + # } + # + # #bar { + # font: 42pt 'Comic Sans MS'; + # } + # ] + # + # Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED) + # + # @return [String] Sanitized CSS stylesheet. + def self.stylesheet(css, config = {}) + new(config).stylesheet(css) + end - # Sanitizes the given Crass CSS parse tree and all its children, modifying it - # in place. - # - # @example - # css = %[ - # .foo { - # background: url(foo.png); - # color: #fff; - # } - # - # #bar { - # font: 42pt 'Comic Sans MS'; - # } - # ] - # - # tree = Crass.parse(css) - # Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED) - # - # @return [Array] Sanitized Crass CSS parse tree. - def self.tree!(tree, config = {}) - self.new(config).tree!(tree) - end + # Sanitizes the given Crass CSS parse tree and all its children, modifying + # it in place. + # + # @example + # css = %[ + # .foo { + # background: url(foo.png); + # color: #fff; + # } + # + # #bar { + # font: 42pt 'Comic Sans MS'; + # } + # ] + # + # tree = Crass.parse(css) + # Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED) + # + # @return [Array] Sanitized Crass CSS parse tree. + def self.tree!(tree, config = {}) + new(config).tree!(tree) + end - # -- Instance Methods -------------------------------------------------------- + # -- Instance Methods ------------------------------------------------------ - # Returns a new Sanitize::CSS object initialized with the settings in - # _config_. - def initialize(config = {}) - @config = Config.merge(Config::DEFAULT[:css], config[:css] || config) + # Returns a new Sanitize::CSS object initialized with the settings in + # _config_. + def initialize(config = {}) + @config = Config.merge(Config::DEFAULT[:css], config[:css] || config) - @at_rules = Set.new(@config[:at_rules]) - @at_rules_with_properties = Set.new(@config[:at_rules_with_properties]) - @at_rules_with_styles = Set.new(@config[:at_rules_with_styles]) - @import_url_validator = @config[:import_url_validator] - end + @at_rules = Set.new(@config[:at_rules]) + @at_rules_with_properties = Set.new(@config[:at_rules_with_properties]) + @at_rules_with_styles = Set.new(@config[:at_rules_with_styles]) + @import_url_validator = @config[:import_url_validator] + end - # Sanitizes inline CSS style properties. - # - # This is most useful for sanitizing non-stylesheet fragments of CSS like you - # would find in the `style` attribute of an HTML element. To sanitize a full - # CSS stylesheet, use {#stylesheet}. - # - # @example - # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED) - # scss.properties("background: url(foo.png); color: #fff;") - # - # @return [String] Sanitized CSS properties. - def properties(css) - tree = Crass.parse_properties(css, - :preserve_comments => @config[:allow_comments], - :preserve_hacks => @config[:allow_hacks]) + # Sanitizes inline CSS style properties. + # + # This is most useful for sanitizing non-stylesheet fragments of CSS like + # you would find in the `style` attribute of an HTML element. To sanitize a + # full CSS stylesheet, use {#stylesheet}. + # + # @example + # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED) + # scss.properties("background: url(foo.png); color: #fff;") + # + # @return [String] Sanitized CSS properties. + def properties(css) + tree = Crass.parse_properties(css, + preserve_comments: @config[:allow_comments], + preserve_hacks: @config[:allow_hacks]) - tree!(tree) - Crass::Parser.stringify(tree) - end + tree!(tree) + Crass::Parser.stringify(tree) + end - # Sanitizes a full CSS stylesheet. - # - # A stylesheet may include selectors, at-rules, and comments. To sanitize only - # inline style properties such as the contents of an HTML `style` attribute, - # use {#properties}. - # - # @example - # css = %[ - # .foo { - # background: url(foo.png); - # color: #fff; - # } - # - # #bar { - # font: 42pt 'Comic Sans MS'; - # } - # ] - # - # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED) - # scss.stylesheet(css) - # - # @return [String] Sanitized CSS stylesheet. - def stylesheet(css) - tree = Crass.parse(css, - :preserve_comments => @config[:allow_comments], - :preserve_hacks => @config[:allow_hacks]) + # Sanitizes a full CSS stylesheet. + # + # A stylesheet may include selectors, at-rules, and comments. To sanitize + # only inline style properties such as the contents of an HTML `style` + # attribute, use {#properties}. + # + # @example + # css = %[ + # .foo { + # background: url(foo.png); + # color: #fff; + # } + # + # #bar { + # font: 42pt 'Comic Sans MS'; + # } + # ] + # + # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED) + # scss.stylesheet(css) + # + # @return [String] Sanitized CSS stylesheet. + def stylesheet(css) + tree = Crass.parse(css, + preserve_comments: @config[:allow_comments], + preserve_hacks: @config[:allow_hacks]) - tree!(tree) - Crass::Parser.stringify(tree) - end + tree!(tree) + Crass::Parser.stringify(tree) + end - # Sanitizes the given Crass CSS parse tree and all its children, modifying it - # in place. - # - # @example - # css = %[ - # .foo { - # background: url(foo.png); - # color: #fff; - # } - # - # #bar { - # font: 42pt 'Comic Sans MS'; - # } - # ] - # - # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED) - # tree = Crass.parse(css) - # - # scss.tree!(tree) - # - # @return [Array] Sanitized Crass CSS parse tree. - def tree!(tree) - preceded_by_property = false + # Sanitizes the given Crass CSS parse tree and all its children, modifying + # it in place. + # + # @example + # css = %[ + # .foo { + # background: url(foo.png); + # color: #fff; + # } + # + # #bar { + # font: 42pt 'Comic Sans MS'; + # } + # ] + # + # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED) + # tree = Crass.parse(css) + # + # scss.tree!(tree) + # + # @return [Array] Sanitized Crass CSS parse tree. + def tree!(tree) + preceded_by_property = false - tree.map! do |node| - next nil if node.nil? + tree.map! do |node| + next nil if node.nil? - case node[:node] - when :at_rule - preceded_by_property = false - next at_rule!(node) + case node[:node] + when :at_rule + preceded_by_property = false + next at_rule!(node) - when :comment - next node if @config[:allow_comments] + when :comment + next node if @config[:allow_comments] - when :property - prop = property!(node) - preceded_by_property = !prop.nil? - next prop + when :property + prop = property!(node) + preceded_by_property = !prop.nil? + next prop - when :semicolon - # Only preserve the semicolon if it was preceded by an allowlisted - # property. Otherwise, omit it in order to prevent redundant semicolons. - if preceded_by_property + when :semicolon + # Only preserve the semicolon if it was preceded by an allowlisted + # property. Otherwise, omit it in order to prevent redundant + # semicolons. + if preceded_by_property + preceded_by_property = false + next node + end + + when :style_rule preceded_by_property = false + tree!(node[:children]) next node + + when :whitespace + next node end - when :style_rule - preceded_by_property = false - tree!(node[:children]) - next node - - when :whitespace - next node + nil end - nil + tree end - tree - end + # -- Protected Instance Methods -------------------------------------------- + protected - # -- Protected Instance Methods ---------------------------------------------- - protected + # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the + # current config doesn't allow this at-rule. + def at_rule!(rule) + name = rule[:name].downcase - # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the - # current config doesn't allow this at-rule. - def at_rule!(rule) - name = rule[:name].downcase + if @at_rules_with_styles.include?(name) + styles = Crass::Parser.parse_rules(rule[:block], + preserve_comments: @config[:allow_comments], + preserve_hacks: @config[:allow_hacks]) - if @at_rules_with_styles.include?(name) - styles = Crass::Parser.parse_rules(rule[:block], - :preserve_comments => @config[:allow_comments], - :preserve_hacks => @config[:allow_hacks]) + rule[:block] = tree!(styles) - rule[:block] = tree!(styles) + elsif @at_rules_with_properties.include?(name) + props = Crass::Parser.parse_properties(rule[:block], + preserve_comments: @config[:allow_comments], + preserve_hacks: @config[:allow_hacks]) - elsif @at_rules_with_properties.include?(name) - props = Crass::Parser.parse_properties(rule[:block], - :preserve_comments => @config[:allow_comments], - :preserve_hacks => @config[:allow_hacks]) + rule[:block] = tree!(props) - rule[:block] = tree!(props) + elsif @at_rules.include?(name) + return nil if name == "import" && !import_url_allowed?(rule) + return nil if rule.has_key?(:block) + else + return nil + end - elsif @at_rules.include?(name) - return nil if name == "import" && !import_url_allowed?(rule) - return nil if rule.has_key?(:block) - else - return nil + rule end - rule - end + # Returns `true` if the given CSS function name is an image-related function + # that may contain image URLs that need to be validated. + def image_function?(name) + ["image", "image-set", "-webkit-image-set"].include?(name) + end - # Returns `true` if the given CSS function name is an image-related function - # that may contain image URLs that need to be validated. - def image_function?(name) - ['image', 'image-set', '-webkit-image-set'].include?(name) - end + # Passes the URL value of an @import rule to a block to ensure + # it's an allowed URL + def import_url_allowed?(rule) + return true unless @import_url_validator - # Passes the URL value of an @import rule to a block to ensure - # it's an allowed URL - def import_url_allowed?(rule) - return true unless @import_url_validator + url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string } - url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string } + # don't allow @imports with no URL value + return false unless url_token && (import_url = url_token[:value]) - # don't allow @imports with no URL value - return false unless url_token && (import_url = url_token[:value]) + @import_url_validator.call(import_url) + end - @import_url_validator.call(import_url) - end + # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the + # current config doesn't allow this property. + def property!(prop) + name = prop[:name].downcase - # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the - # current config doesn't allow this property. - def property!(prop) - name = prop[:name].downcase + # Preserve IE * and _ hacks if desired. + if @config[:allow_hacks] + name.slice!(0) if /\A[*_]/.match?(name) + end - # Preserve IE * and _ hacks if desired. - if @config[:allow_hacks] - name.slice!(0) if name =~ /\A[*_]/ - end + return nil unless @config[:properties].include?(name) - return nil unless @config[:properties].include?(name) + nodes = prop[:children].dup + combined_value = +"" - nodes = prop[:children].dup - combined_value = String.new + nodes.each do |child| + value = child[:value] - nodes.each do |child| - value = child[:value] + case child[:node] + when :ident + combined_value << value.downcase if String === value - case child[:node] - when :ident - combined_value << value.downcase if String === value + when :function + if child.key?(:name) + name = child[:name].downcase - when :function - if child.key?(:name) - name = child[:name].downcase + if name == "url" + return nil unless valid_url?(child) + end - if name == 'url' - return nil unless valid_url?(child) + if image_function?(name) + return nil unless valid_image?(child) + end + + combined_value << name + return nil if name == "expression" || combined_value == "expression" end - if image_function?(name) - return nil unless valid_image?(child) + if Array === value + nodes.concat(value) + elsif String === value + lowercase_value = value.downcase + combined_value << lowercase_value + return nil if lowercase_value == "expression" || combined_value == "expression" end - combined_value << name - return nil if name == 'expression' || combined_value == 'expression' - end + when :url + return nil unless valid_url?(child) - if Array === value - nodes.concat(value) - elsif String === value - lowercase_value = value.downcase - combined_value << lowercase_value - return nil if lowercase_value == 'expression' || combined_value == 'expression' + when :bad_url + return nil end - - when :url - return nil unless valid_url?(child) - - when :bad_url - return nil end + + prop end - prop - end + # Returns `true` if the given node (which may be of type `:url` or + # `:function`, since the CSS syntax can produce both) uses an allowlisted + # protocol. + def valid_url?(node) + type = node[:node] - # Returns `true` if the given node (which may be of type `:url` or - # `:function`, since the CSS syntax can produce both) uses an allowlisted - # protocol. - def valid_url?(node) - type = node[:node] + if type == :function + return false unless node.key?(:name) && node[:name].downcase == "url" + return false unless Array === node[:value] - if type == :function - return false unless node.key?(:name) && node[:name].downcase == 'url' - return false unless Array === node[:value] + # A URL function's `:value` should be an array containing no more than + # one `:string` node and any number of `:whitespace` nodes. + # + # If it contains more than one `:string` node, or if it contains any + # other nodes except `:whitespace` nodes, it's not valid. + url_string_node = nil - # A URL function's `:value` should be an array containing no more than one - # `:string` node and any number of `:whitespace` nodes. - # - # If it contains more than one `:string` node, or if it contains any other - # nodes except `:whitespace` nodes, it's not valid. - url_string_node = nil + node[:value].each do |token| + return false unless Hash === token - node[:value].each do |token| - return false unless Hash === token - - case token[:node] + case token[:node] when :string return false unless url_string_node.nil? url_string_node = token when :whitespace next else return false + end end + + return false if url_string_node.nil? + url = url_string_node[:value] + elsif type == :url + url = node[:value] + else + return false end - return false if url_string_node.nil? - url = url_string_node[:value] - elsif type == :url - url = node[:value] - else - return false + if url =~ Sanitize::REGEX_PROTOCOL + @config[:protocols].include?($1.downcase) + else + @config[:protocols].include?(:relative) + end end - if url =~ Sanitize::REGEX_PROTOCOL - return @config[:protocols].include?($1.downcase) - else - return @config[:protocols].include?(:relative) - end + # Returns `true` if the given node is an image-related function and contains + # only strings that use an allowlisted protocol. + def valid_image?(node) + return false unless node[:node] == :function + return false unless node.key?(:name) && image_function?(node[:name].downcase) + return false unless Array === node[:value] - false - end - - # Returns `true` if the given node is an image-related function and contains - # only strings that use an allowlisted protocol. - def valid_image?(node) - return false unless node[:node] == :function - return false unless node.key?(:name) && image_function?(node[:name].downcase) - return false unless Array === node[:value] - - node[:value].each do |token| + node[:value].each do |token| return false unless Hash === token case token[:node] - when :string - if token[:value] =~ Sanitize::REGEX_PROTOCOL - return false unless @config[:protocols].include?($1.downcase) - else - return false unless @config[:protocols].include?(:relative) - end + when :string + if token[:value] =~ Sanitize::REGEX_PROTOCOL + return false unless @config[:protocols].include?($1.downcase) else - next + return false unless @config[:protocols].include?(:relative) + end + else + next end end + end end - -end; end +end