# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# This cop checks for the use of output safety calls like `html_safe`,
# `raw`, and `safe_concat`. These methods do not escape content. They
# simply return a SafeBuffer containing the content as is. Instead,
# use `safe_join` to join content and escape it and concat to
# concatenate content and escape it, ensuring its safety.
#
# @example
# user_content = "hi"
#
# # bad
# "
#{user_content}
".html_safe
# # => ActiveSupport::SafeBuffer "hi
"
#
# # good
# content_tag(:p, user_content)
# # => ActiveSupport::SafeBuffer "<b>hi</b>
"
#
# # bad
# out = ""
# out << "#{user_content}"
# out << "#{user_content}"
# out.html_safe
# # => ActiveSupport::SafeBuffer "hihi"
#
# # good
# out = []
# out << content_tag(:li, user_content)
# out << content_tag(:li, user_content)
# safe_join(out)
# # => ActiveSupport::SafeBuffer
# # "<b>hi</b><b>hi</b>"
#
# # bad
# out = "trusted content
".html_safe
# out.safe_concat(user_content)
# # => ActiveSupport::SafeBuffer "trusted_content
hi"
#
# # good
# out = "trusted content
".html_safe
# out.concat(user_content)
# # => ActiveSupport::SafeBuffer
# # "trusted_content
<b>hi</b>"
#
# # safe, though maybe not good style
# out = "trusted content"
# result = out.concat(user_content)
# # => String "trusted contenthi"
# # because when rendered in ERB the String will be escaped:
# # <%= result %>
# # => trusted content<b>hi</b>
#
# # bad
# (user_content + " " + content_tag(:span, user_content)).html_safe
# # => ActiveSupport::SafeBuffer "hi hi"
#
# # good
# safe_join([user_content, " ", content_tag(:span, user_content)])
# # => ActiveSupport::SafeBuffer
# # "<b>hi</b> <b>hi</b>"
class OutputSafety < Cop
MSG = 'Tagging a string as html safe may be a security risk.'.freeze
def on_send(node)
return if non_interpolated_string?(node)
return unless looks_like_rails_html_safe?(node) ||
looks_like_rails_raw?(node) ||
looks_like_rails_safe_concat?(node)
add_offense(node, location: :selector)
end
alias on_csend on_send
private
def non_interpolated_string?(node)
node.receiver && node.receiver.str_type? && !node.receiver.dstr_type?
end
def looks_like_rails_html_safe?(node)
node.receiver && node.method?(:html_safe) && !node.arguments?
end
def looks_like_rails_raw?(node)
node.command?(:raw) && node.arguments.one?
end
def looks_like_rails_safe_concat?(node)
node.method?(:safe_concat) && node.arguments.one?
end
end
end
end
end