From 4a0ed3a2c20173fd728557598b496fa99ccbd3b7 Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Sun, 9 Aug 2009 12:10:16 +1200 Subject: [PATCH] Introduce the concept of "HTML Safe" for strings * Strings are marked safe with String#html_safe! * SafeBuffer is a new buffer class which escapes all non-safe strings * Add a raw helper which marks strings as safe Note, this change does *not* impact rendering in any way, you must install the rails_xss plugin to get that behaviour. --- actionpack/lib/action_view.rb | 6 +- actionpack/lib/action_view/erb/util.rb | 5 + actionpack/lib/action_view/helpers.rb | 2 + .../action_view/helpers/active_record_helper.rb | 2 +- .../lib/action_view/helpers/asset_tag_helper.rb | 4 +- .../lib/action_view/helpers/capture_helper.rb | 4 +- actionpack/lib/action_view/helpers/date_helper.rb | 6 +- actionpack/lib/action_view/helpers/form_helper.rb | 4 +- .../lib/action_view/helpers/form_tag_helper.rb | 6 +- .../lib/action_view/helpers/prototype_helper.rb | 2 +- .../lib/action_view/helpers/raw_output_helper.rb | 9 ++ .../lib/action_view/helpers/sanitize_helper.rb | 12 +++- actionpack/lib/action_view/helpers/tag_helper.rb | 8 +- actionpack/lib/action_view/helpers/url_helper.rb | 6 +- actionpack/lib/action_view/partials.rb | 2 +- actionpack/lib/action_view/safe_buffer.rb | 28 +++++++ actionpack/test/template/asset_tag_helper_test.rb | 12 +++ actionpack/test/template/form_helper_test.rb | 2 +- actionpack/test/template/raw_output_helper_test.rb | 21 +++++ actionpack/test/template/sanitize_helper_test.rb | 11 +++- actionpack/test/template/tag_helper_test.rb | 1 + actionpack/test/view/safe_buffer_test.rb | 36 ++++++++ .../lib/active_support/core_ext/string.rb | 2 + .../core_ext/string/output_safety.rb | 48 +++++++++++ activesupport/test/core_ext/string_ext_test.rb | 86 ++++++++++++++++++++ 25 files changed, 296 insertions(+), 29 deletions(-) create mode 100644 actionpack/lib/action_view/helpers/raw_output_helper.rb create mode 100644 actionpack/lib/action_view/safe_buffer.rb create mode 100644 actionpack/test/template/raw_output_helper_test.rb create mode 100644 actionpack/test/view/safe_buffer_test.rb create mode 100644 activesupport/lib/active_support/core_ext/string/output_safety.rb diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 1f1ff9d..025745c 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -49,10 +49,10 @@ module ActionView autoload :TemplateHandler, 'action_view/template_handler' autoload :TemplateHandlers, 'action_view/template_handlers' autoload :Helpers, 'action_view/helpers' + autoload :SafeBuffer, 'action_view/safe_buffer' end -class ERB - autoload :Util, 'action_view/erb/util' -end +require 'action_view/erb/util' + I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" diff --git a/actionpack/lib/action_view/erb/util.rb b/actionpack/lib/action_view/erb/util.rb index 3c77c5c..3bf877f 100644 --- a/actionpack/lib/action_view/erb/util.rb +++ b/actionpack/lib/action_view/erb/util.rb @@ -18,6 +18,11 @@ class ERB s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] } end + alias h html_escape + + module_function :html_escape + module_function :h + # A utility method for escaping HTML entities in JSON strings. # This method is also aliased as j. # diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index 97fa2d8..cea894d 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -14,6 +14,7 @@ module ActionView #:nodoc: autoload :JavaScriptHelper, 'action_view/helpers/javascript_helper' autoload :NumberHelper, 'action_view/helpers/number_helper' autoload :PrototypeHelper, 'action_view/helpers/prototype_helper' + autoload :RawOutputHelper, 'action_view/helpers/raw_output_helper' autoload :RecordIdentificationHelper, 'action_view/helpers/record_identification_helper' autoload :RecordTagHelper, 'action_view/helpers/record_tag_helper' autoload :SanitizeHelper, 'action_view/helpers/sanitize_helper' @@ -45,6 +46,7 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include PrototypeHelper + include RawOutputHelper include RecordIdentificationHelper include RecordTagHelper include SanitizeHelper diff --git a/actionpack/lib/action_view/helpers/active_record_helper.rb b/actionpack/lib/action_view/helpers/active_record_helper.rb index 541899e..355f098 100644 --- a/actionpack/lib/action_view/helpers/active_record_helper.rb +++ b/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -290,7 +290,7 @@ module ActionView end def error_wrapping(html_tag, has_error) - has_error ? Base.field_error_proc.call(html_tag, self) : html_tag + has_error ? Base.field_error_proc.call(html_tag, self).html_safe! : html_tag end def error_message diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index babb9db..574b384 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -285,7 +285,7 @@ module ActionView end javascript_src_tag(joined_javascript_name, options) else - expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n") + expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe! end end @@ -434,7 +434,7 @@ module ActionView end stylesheet_tag(joined_stylesheet_name, options) else - expand_stylesheet_sources(sources, recursive).collect { |source| stylesheet_tag(source, options) }.join("\n") + expand_stylesheet_sources(sources, recursive).collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe! end end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index e86ca27..40411c2 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -118,13 +118,13 @@ module ActionView def content_for(name, content = nil, &block) ivar = "@content_for_#{name}" content = capture(&block) if block_given? - instance_variable_set(ivar, "#{instance_variable_get(ivar)}#{content}") + instance_variable_set(ivar, "#{instance_variable_get(ivar)}#{content}".html_safe!) nil end # Use an alternate output buffer for the duration of the block. # Defaults to a new empty string. - def with_output_buffer(buf = '') #:nodoc: + def with_output_buffer(buf = "") #:nodoc: self.output_buffer, old_buffer = buf, output_buffer yield output_buffer diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index c74909a..0ad0a07 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -904,15 +904,15 @@ module ActionView class InstanceTag #:nodoc: def to_date_select_tag(options = {}, html_options = {}) - datetime_selector(options, html_options).select_date + datetime_selector(options, html_options).select_date.html_safe! end def to_time_select_tag(options = {}, html_options = {}) - datetime_selector(options, html_options).select_time + datetime_selector(options, html_options).select_time.html_safe! end def to_datetime_select_tag(options = {}, html_options = {}) - datetime_selector(options, html_options).select_datetime + datetime_selector(options, html_options).select_datetime.html_safe! end private diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 2ac407c..431c153 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -280,7 +280,7 @@ module ActionView concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {})) fields_for(object_name, *(args << options), &proc) - concat('') + concat(''.html_safe!) end def apply_form_for_options!(object_or_array, options) #:nodoc: @@ -787,7 +787,7 @@ module ActionView add_default_name_and_id(options) hidden = tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value) checkbox = tag("input", options) - hidden + checkbox + (hidden + checkbox).html_safe! end def to_boolean_select_tag(options = {}) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index c217191..eaba603 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -432,7 +432,7 @@ module ActionView concat(tag(:fieldset, options, true)) concat(content_tag(:legend, legend)) unless legend.blank? concat(content) - concat("") + concat("".html_safe!) end private @@ -459,14 +459,14 @@ module ActionView def form_tag_html(html_options) extra_tags = extra_tags_for_form(html_options) - tag(:form, html_options, true) + extra_tags + (tag(:form, html_options, true) + extra_tags).html_safe! end def form_tag_in_block(html_options, &block) content = capture(&block) concat(form_tag_html(html_options)) concat(content) - concat("") + concat("".html_safe!) end def token_tag diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 448d6f5..75f5c86 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -393,7 +393,7 @@ module ActionView concat(form_remote_tag(options)) fields_for(object_name, *(args << options), &proc) - concat('') + concat(''.html_safe!) end alias_method :form_remote_for, :remote_form_for diff --git a/actionpack/lib/action_view/helpers/raw_output_helper.rb b/actionpack/lib/action_view/helpers/raw_output_helper.rb new file mode 100644 index 0000000..79b0e4e --- /dev/null +++ b/actionpack/lib/action_view/helpers/raw_output_helper.rb @@ -0,0 +1,9 @@ +module ActionView #:nodoc: + module Helpers #:nodoc: + module RawOutputHelper + def raw(stringish) + stringish.to_s.html_safe! + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index d89b955..69d0d0f 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -49,7 +49,11 @@ module ActionView # confuse browsers. # def sanitize(html, options = {}) - self.class.white_list_sanitizer.sanitize(html, options) + returning self.class.white_list_sanitizer.sanitize(html, options) do |sanitized| + if sanitized + sanitized.html_safe! + end + end end # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute. @@ -72,7 +76,11 @@ module ActionView # strip_tags("
Welcome to my website!
") # # => Welcome to my website! def strip_tags(html) - self.class.full_sanitizer.sanitize(html) + returning self.class.full_sanitizer.sanitize(html) do |sanitized| + if sanitized + sanitized.html_safe! + end + end end # Strips all link tags from +text+ leaving just the link text. diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index af8c4d5..0e8988d 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -38,7 +38,7 @@ module ActionView # tag("img", { :src => "open & shut.png" }, false, false) # # => def tag(name, options = nil, open = false, escape = true) - "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}" + "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe! end # Returns an HTML block tag of type +name+ surrounding the +content+. Add @@ -91,7 +91,7 @@ module ActionView # cdata_section(File.read("hello_world.txt")) # # => def cdata_section(content) - "" + "".html_safe! end # Returns an escaped version of +html+ without affecting existing escaped entities. @@ -125,7 +125,7 @@ module ActionView def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - "<#{name}#{tag_options}>#{content}" + "<#{name}#{tag_options}>#{content}".html_safe! end def tag_options(options, escape = true) @@ -142,7 +142,7 @@ module ActionView else attrs = options.map { |key, value| %(#{key}="#{value}") } end - " #{attrs.sort * ' '}" unless attrs.empty? + " #{attrs.sort * ' '}".html_safe! unless attrs.empty? end end end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 9270886..8617c56 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -219,7 +219,7 @@ module ActionView if block_given? options = args.first || {} html_options = args.second - concat(link_to(capture(&block), options, html_options)) + concat(link_to(capture(&block), options, html_options).html_safe!) else name = args.first options = args.second || {} @@ -237,7 +237,7 @@ module ActionView end href_attr = "href=\"#{url}\"" unless href - "#{name || url}" + "#{name || url}".html_safe! end end @@ -309,7 +309,7 @@ module ActionView html_options.merge!("type" => "submit", "value" => name) "
" + - method_tag + tag("input", html_options) + request_token_tag + "
" + method_tag + tag("input", html_options) + request_token_tag + "".html_safe! end diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index 9e5e0f7..5d75dbb 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -221,7 +221,7 @@ module ActionView result = template.render_partial(self, object, local_assigns.dup, as) index += 1 result - end.join(spacer) + end.join(spacer).html_safe! end def _pick_partial_template(partial_path) #:nodoc: diff --git a/actionpack/lib/action_view/safe_buffer.rb b/actionpack/lib/action_view/safe_buffer.rb new file mode 100644 index 0000000..09f44ab --- /dev/null +++ b/actionpack/lib/action_view/safe_buffer.rb @@ -0,0 +1,28 @@ + +module ActionView #:nodoc: + class SafeBuffer < String + def <<(value) + if value.html_safe? + super(value) + else + super(ERB::Util.h(value)) + end + end + + def concat(value) + self << value + end + + def html_safe? + true + end + + def html_safe! + self + end + + def to_s + self + end + end +end \ No newline at end of file diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 7ffabff..46e6129 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -164,6 +164,11 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal(%(\n\n\n\n), javascript_include_tag(:defaults)) end + def test_javascript_include_tag_is_html_safe + assert javascript_include_tag(:defaults).html_safe? + assert javascript_include_tag("prototype").html_safe? + end + def test_register_javascript_include_default ENV["RAILS_ASSET_ID"] = "" ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider' @@ -206,6 +211,13 @@ class AssetTagHelperTest < ActionView::TestCase StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end + def test_stylesheet_link_tag_is_html_safe + ENV["RAILS_ASSET_ID"] = "" + assert stylesheet_link_tag('dir/file').html_safe? + assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe? + assert stylesheet_tag('dir/file', {}).html_safe? + end + def test_custom_stylesheet_expansions ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => ["head", "body", "tail"] assert_dom_equal %(\n\n\n\n), stylesheet_link_tag('first', :monkey, 'last') diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index e92f62d..b554800 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -998,7 +998,7 @@ class FormHelperTest < ActionView::TestCase (field_helpers - %w(hidden_field)).each do |selector| src = <<-END_SRC def #{selector}(field, *args, &proc) - " " + super + "
" + (" " + super + "
").html_safe! end END_SRC class_eval src, __FILE__, __LINE__ diff --git a/actionpack/test/template/raw_output_helper_test.rb b/actionpack/test/template/raw_output_helper_test.rb new file mode 100644 index 0000000..598aa5b --- /dev/null +++ b/actionpack/test/template/raw_output_helper_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' +require 'testing_sandbox' + +class RawOutputHelperTest < ActionView::TestCase + tests ActionView::Helpers::RawOutputHelper + include TestingSandbox + + def setup + @string = "hello" + end + + test "raw returns the safe string" do + result = raw(@string) + assert_equal @string, result + assert result.html_safe? + end + + test "raw handles nil values correctly" do + assert_equal "", raw(nil) + end +end \ No newline at end of file diff --git a/actionpack/test/template/sanitize_helper_test.rb b/actionpack/test/template/sanitize_helper_test.rb index f715071..222d4db 100644 --- a/actionpack/test/template/sanitize_helper_test.rb +++ b/actionpack/test/template/sanitize_helper_test.rb @@ -39,7 +39,16 @@ class SanitizeHelperTest < ActionView::TestCase %{This is a test.\n\n\nIt no longer contains any HTML.\n}, strip_tags( %{This is <b>a <a href="" target="_blank">test</a></b>.\n\n\n\n

It no longer contains any HTML.

\n})) assert_equal "This has a here.", strip_tags("This has a here.") - [nil, '', ' '].each { |blank| assert_equal blank, strip_tags(blank) } + [nil, '', ' '].each do |blank| + stripped = strip_tags(blank) + assert_equal blank, stripped + assert stripped.html_safe? unless blank.nil? + end + assert strip_tags("").html_safe? end def assert_sanitized(text, expected = nil) diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index ef88cae..2acf6af 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -34,6 +34,7 @@ class TagHelperTest < ActionView::TestCase def test_content_tag assert_equal "Create", content_tag("a", "Create", "href" => "create") + assert content_tag("a", "Create", "href" => "create").html_safe? assert_equal content_tag("a", "Create", "href" => "create"), content_tag("a", "Create", :href => "create") end diff --git a/actionpack/test/view/safe_buffer_test.rb b/actionpack/test/view/safe_buffer_test.rb new file mode 100644 index 0000000..0b378ae --- /dev/null +++ b/actionpack/test/view/safe_buffer_test.rb @@ -0,0 +1,36 @@ +require 'abstract_unit' + +class SafeBufferTest < ActionView::TestCase + def setup + @buffer = ActionView::SafeBuffer.new + end + + test "Should look like a string" do + assert @buffer.is_a?(String) + assert_equal "", @buffer + end + + test "Should escape a raw string which is passed to them" do + @buffer << "