require_relative 'base' require 'better_html/test_helper/ruby_node' module BetterHtml module TestHelper module SafeErb class TagInterpolation < Base NO_HTML_TAGS = %w( title textarea script style xmp iframe noembed noframes listing plaintext ) def validate @parser.nodes_with_type(:tag).each do |tag_node| tag = Tree::Tag.from_node(tag_node) tag.attributes.each do |attribute| validate_attribute(attribute) end end @parser.nodes_with_type(:text).each do |node| validate_text_node(node) unless no_html_tag?(node) end end private def no_html_tag?(node) ast = @parser.ast.to_a index = ast.find_index(node) return unless (previous_node = ast[index - 1]) return unless previous_node.type == :tag tag = BetterHtml::Tree::Tag.from_node(previous_node) NO_HTML_TAGS.include?(tag.name) && !tag.closing? end def validate_attribute(attribute) erb_nodes(attribute.value_node).each do |erb_node, indicator_node, code_node| next if indicator_node.nil? indicator = indicator_node.loc.source source = code_node.loc.source if indicator == '=' if (ruby_node = RubyNode.parse(source)) no_unsafe_calls(code_node, ruby_node) unless ruby_node.static_return_value? handle_missing_safe_wrapper(code_node, ruby_node, attribute.name) end end elsif indicator == '==' add_error( "erb interpolation with '<%==' inside html attribute is never safe", location: erb_node.loc ) end end end def validate_text_node(text_node) erb_nodes(text_node).each do |erb_node, indicator_node, code_node| indicator = indicator_node&.loc&.source next if indicator == '#' source = code_node.loc.source next unless (ruby_node = RubyNode.parse(source)) no_unsafe_calls(code_node, ruby_node) validate_ruby_helper(code_node, ruby_node) end end def validate_ruby_helper(parent_node, ruby_node) ruby_node.descendants(:send, :csend).each do |send_node| send_node.descendants(:hash).each do |hash_node| hash_node.child_nodes.select(&:pair?).each do |pair_node| validate_ruby_helper_hash_entry(parent_node, ruby_node, nil, *pair_node.children) end end end end def validate_ruby_helper_hash_entry(parent_node, ruby_node, key_prefix, key_node, value_node) return unless [:sym, :str].include?(key_node.type) key = [key_prefix, key_node.children.first.to_s].compact.join('-').dasherize case value_node.type when :dstr validate_ruby_helper_hash_value(parent_node, ruby_node, key, value_node) when :hash if key == 'data' value_node.child_nodes.select(&:pair?).each do |pair_node| validate_ruby_helper_hash_entry(parent_node, ruby_node, key, *pair_node.children) end end end end def validate_ruby_helper_hash_value(parent_node, ruby_node, attr_name, hash_value) hash_value.child_nodes.select(&:begin?).each do |begin_node| validate_tag_interpolation(parent_node, begin_node, attr_name) end end def handle_missing_safe_wrapper(parent_node, ruby_node, attr_name) return unless @config.javascript_attribute_name?(attr_name) method_calls = ruby_node.return_values.select(&:method_call?) unsafe_calls = method_calls.select { |node| !@config.javascript_safe_method?(node.method_name) } if method_calls.empty? add_error( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", location: nested_location(parent_node, ruby_node) ) true elsif unsafe_calls.any? unsafe_calls.each do |call_node| add_error( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", location: nested_location(parent_node, call_node) ) end true end end def validate_tag_interpolation(parent_node, ruby_node, attr_name) return if ruby_node.static_return_value? return if handle_missing_safe_wrapper(parent_node, ruby_node, attr_name) ruby_node.return_values.each do |call_node| next if call_node.static_return_value? if @config.javascript_attribute_name?(attr_name) && !@config.javascript_safe_method?(call_node.method_name) add_error( "erb interpolation in javascript attribute must be wrapped in safe helper such as '(...).to_json'", location: nested_location(parent_node, ruby_node) ) end end end def no_unsafe_calls(parent_node, ruby_node) ruby_node.descendants(:send, :csend).each do |call| if call.method_name?(:raw) add_error( "erb interpolation with '<%= raw(...) %>' in this context is never safe", location: nested_location(parent_node, ruby_node) ) elsif call.method_name?(:html_safe) add_error( "erb interpolation with '<%= (...).html_safe %>' in this context is never safe", location: nested_location(parent_node, ruby_node) ) end end end def nested_location(parent_node, ruby_node) Tokenizer::Location.new( parent_node.loc.source_buffer, parent_node.loc.begin_pos + ruby_node.loc.expression.begin_pos, parent_node.loc.begin_pos + ruby_node.loc.expression.end_pos ) end end end end end