Skip to content
Permalink
Browse files

Switch to on-by-default XSS escaping for rails.

  This consists of:

  * String#html_safe! a method to mark a string as 'safe'
  * ActionView::SafeBuffer a string subclass which escapes anything unsafe which is concatenated to it
  * Calls to String#html_safe! throughout the rails helpers
  * a 'raw' helper which lets you concatenate trusted HTML from non-safety-aware sources (e.g. presantized strings in the DB)
  * New ERB implementation based on erubis which uses a SafeBuffer instead of a String

Hat tip to Django for the inspiration.
  • Loading branch information...
NZKoz committed Oct 7, 2009
1 parent f27e7eb commit 9415935902f120a9bac0bfce7129725a0db38ed3
Showing with 368 additions and 42 deletions.
  1. +1 −0 actionpack/Gemfile
  2. +3 −3 actionpack/lib/action_view.rb
  3. +1 −1 actionpack/lib/action_view/base.rb
  4. +11 −1 actionpack/lib/action_view/erb/util.rb
  5. +2 −0 actionpack/lib/action_view/helpers.rb
  6. +1 −0 actionpack/lib/action_view/helpers/active_model_helper.rb
  7. +3 −3 actionpack/lib/action_view/helpers/asset_tag_helper.rb
  8. +1 −1 actionpack/lib/action_view/helpers/capture_helper.rb
  9. +3 −3 actionpack/lib/action_view/helpers/date_helper.rb
  10. +2 −2 actionpack/lib/action_view/helpers/form_helper.rb
  11. +1 −1 actionpack/lib/action_view/helpers/form_options_helper.rb
  12. +3 −3 actionpack/lib/action_view/helpers/form_tag_helper.rb
  13. +1 −1 actionpack/lib/action_view/helpers/prototype_helper.rb
  14. +9 −0 actionpack/lib/action_view/helpers/raw_output_helper.rb
  15. +10 −2 actionpack/lib/action_view/helpers/sanitize_helper.rb
  16. +4 −4 actionpack/lib/action_view/helpers/tag_helper.rb
  17. +5 −5 actionpack/lib/action_view/helpers/url_helper.rb
  18. +1 −1 actionpack/lib/action_view/render/partials.rb
  19. +28 −0 actionpack/lib/action_view/safe_buffer.rb
  20. +25 −3 actionpack/lib/action_view/template/handlers/erb.rb
  21. +1 −1 actionpack/lib/action_view/test_case.rb
  22. +19 −0 actionpack/test/controller/output_escaping_test.rb
  23. +1 −1 actionpack/test/controller/render_test.rb
  24. +12 −0 actionpack/test/template/asset_tag_helper_test.rb
  25. +12 −0 actionpack/test/template/erb_util_test.rb
  26. +1 −1 actionpack/test/template/form_helper_test.rb
  27. +21 −0 actionpack/test/template/raw_output_helper_test.rb
  28. +1 −1 actionpack/test/template/render_test.rb
  29. +10 −1 actionpack/test/template/sanitize_helper_test.rb
  30. +1 −0 actionpack/test/template/tag_helper_test.rb
  31. +1 −1 actionpack/test/template/test_case_test.rb
  32. +1 −1 actionpack/test/template/url_helper_test.rb
  33. +41 −0 actionpack/test/view/safe_buffer_test.rb
  34. +2 −1 activesupport/lib/active_support/core_ext/string.rb
  35. +43 −0 activesupport/lib/active_support/core_ext/string/output_safety.rb
  36. +86 −0 activesupport/test/core_ext/string_ext_test.rb
@@ -4,6 +4,7 @@ gem "rack", "~> 1.0.0"
gem "rack-test", "~> 0.5.0"
gem "activesupport", "3.0.pre", :vendored_at => rails_root.join("activesupport")
gem "activemodel", "3.0.pre", :vendored_at => rails_root.join("activemodel")
gem "erubis", "~> 2.6.0"

only :test do
gem "mocha"
@@ -44,11 +44,11 @@ def self.load_all!
autoload :TextTemplate, 'action_view/template/text'
autoload :Helpers, 'action_view/helpers'
autoload :FileSystemResolverWithFallback, 'action_view/template/resolver'
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"

@@ -260,7 +260,7 @@ def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil,
@assigns = assigns_for_first_render.each { |key, value| instance_variable_set("@#{key}", value) }
@controller = controller
@helpers = self.class.helpers || Module.new
@_content_for = Hash.new {|h,k| h[k] = "" }
@_content_for = Hash.new {|h,k| h[k] = ActionView::SafeBuffer.new }
self.view_paths = view_paths
end

@@ -15,9 +15,19 @@ module Util
# puts html_escape("is a > 0 & a < 10?")
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
s = s.to_s
if s.html_safe?
s
else
s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }.html_safe!
end
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 <tt>j</tt>.
#
@@ -15,6 +15,7 @@ module Helpers #: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'
@@ -46,6 +47,7 @@ module ClassMethods
include JavaScriptHelper
include NumberHelper
include PrototypeHelper
include RawOutputHelper
include RecordIdentificationHelper
include RecordTagHelper
include SanitizeHelper
@@ -91,6 +91,7 @@ def form(record_name, options = {})
yield contents if block_given?
contents << submit_tag(submit_value)
contents << '</form>'
contents.html_safe!
end

# Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
@@ -289,7 +289,7 @@ def javascript_include_tag(*sources)
else
sources = expand_javascript_sources(sources, recursive)
ensure_javascript_sources!(sources) if cache
sources.collect { |source| javascript_src_tag(source, options) }.join("\n")
sources.collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe!
end
end

@@ -440,7 +440,7 @@ def stylesheet_link_tag(*sources)
else
sources = expand_stylesheet_sources(sources, recursive)
ensure_stylesheet_sources!(sources) if cache
sources.collect { |source| stylesheet_tag(source, options) }.join("\n")
sources.collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe!
end
end

@@ -584,7 +584,7 @@ def video_tag(sources, options = {})

if sources.is_a?(Array)
content_tag("video", options) do
sources.map { |source| tag("source", :src => source) }.join
sources.map { |source| tag("source", :src => source) }.join.html_safe!
end
else
options[:src] = path_to_video(sources)
@@ -143,7 +143,7 @@ def content_for?(name)
# Defaults to a new empty string.
def with_output_buffer(buf = nil) #:nodoc:
unless buf
buf = ''
buf = ActionView::SafeBuffer.new
buf.force_encoding(output_buffer.encoding) if buf.respond_to?(:force_encoding)
end
self.output_buffer, old_buffer = buf, output_buffer
@@ -916,15 +916,15 @@ def separator(type)

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
@@ -282,7 +282,7 @@ def form_for(record_or_name_or_array, *args, &proc)

concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}))
fields_for(object_name, *(args << options), &proc)
concat('</form>')
concat('</form>'.html_safe!)
end

def apply_form_for_options!(object_or_array, options) #:nodoc:
@@ -809,7 +809,7 @@ def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
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 = {})
@@ -296,7 +296,7 @@ def options_for_select(container, selected = nil)
options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}>#{html_escape(text.to_s)}</option>)
end

options_for_select.join("\n")
options_for_select.join("\n").html_safe!
end

# Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
@@ -440,7 +440,7 @@ def field_set_tag(legend = nil, options = nil, &block)
concat(tag(:fieldset, options, true))
concat(content_tag(:legend, legend)) unless legend.blank?
concat(content)
concat("</fieldset>")
concat("</fieldset>".html_safe!)
end

private
@@ -467,14 +467,14 @@ def extra_tags_for_form(html_options)

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("</form>")
concat("</form>".html_safe!)
end

def token_tag
@@ -395,7 +395,7 @@ def remote_form_for(record_or_name_or_array, *args, &proc)

concat(form_remote_tag(options))
fields_for(object_name, *(args << options), &proc)
concat('</form>')
concat('</form>'.html_safe!)
end
alias_method :form_remote_for, :remote_form_for

@@ -0,0 +1,9 @@
module ActionView #:nodoc:
module Helpers #:nodoc:
module RawOutputHelper
def raw(stringish)
stringish.to_s.html_safe!
end
end
end
end
@@ -49,7 +49,11 @@ module SanitizeHelper
# 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 @@ def sanitize_css(style)
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => 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.
@@ -41,7 +41,7 @@ module TagHelper
# tag("img", { :src => "open &amp; shut.png" }, false, false)
# # => <img src="open &amp; shut.png" />
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
@@ -94,7 +94,7 @@ def content_tag(name, content_or_options_with_block = nil, options = nil, escape
# cdata_section(File.read("hello_world.txt"))
# # => <![CDATA[<hello from a text file]]>
def cdata_section(content)
"<![CDATA[#{content}]]>"
"<![CDATA[#{content}]]>".html_safe!
end

# Returns an escaped version of +html+ without affecting existing escaped entities.
@@ -128,7 +128,7 @@ def block_called_from_erb?(block)

def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
"<#{name}#{tag_options}>#{content}</#{name}>"
"<#{name}#{tag_options}>#{content}</#{name}>".html_safe!
end

def tag_options(options, escape = true)
@@ -143,7 +143,7 @@ def tag_options(options, escape = true)
attrs << %(#{key}="#{final_value}")
end
end
" #{attrs.sort * ' '}" unless attrs.empty?
" #{attrs.sort * ' '}".html_safe! unless attrs.empty?
end
end
end
@@ -93,7 +93,7 @@ def url_for(options = {})
polymorphic_path(options)
end

escape ? escape_once(url) : url
(escape ? escape_once(url) : url).html_safe!
end

# Creates a link tag of the given +name+ using a URL created by the set
@@ -220,7 +220,7 @@ def link_to(*args, &block)
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[0]
options = args[1] || {}
@@ -238,7 +238,7 @@ def link_to(*args, &block)
end

href_attr = "href=\"#{url}\"" unless href
"<a #{href_attr}#{tag_options}>#{name || url}</a>"
"<a #{href_attr}#{tag_options}>#{ERB::Util.h(name || url)}</a>".html_safe!
end
end

@@ -309,8 +309,8 @@ def button_to(name, options = {}, html_options = {})

html_options.merge!("type" => "submit", "value" => name)

"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + tag("input", html_options) + request_token_tag + "</div></form>"
("<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + tag("input", html_options) + request_token_tag + "</div></form>").html_safe!
end


@@ -223,7 +223,7 @@ def render_collection
end

result = template ? collection_with_template(template) : collection_without_template
result.join(spacer)
result.join(spacer).html_safe!
end

def collection_with_template(template)
@@ -0,0 +1,28 @@

module ActionView #:nodoc:
class SafeBuffer < String
def <<(value)
if value.html_safe?
super(value)
else
super(CGI.escapeHTML(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
@@ -1,7 +1,31 @@
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/string/output_safety'
require 'erubis'

module ActionView
module TemplateHandlers
class Erubis < ::Erubis::Eruby
def add_preamble(src)
src << "@output_buffer = ActionView::SafeBuffer.new;"
end

def add_text(src, text)
src << "@output_buffer << ('" << escape_text(text) << "'.html_safe!);"
end

def add_expr_literal(src, code)
src << '@output_buffer << ((' << code << ').to_s);'
end

def add_expr_escaped(src, code)
src << '@output_buffer << ' << escaped_expr(code) << ';'
end

def add_postamble(src)
src << '@output_buffer.to_s'
end
end

class ERB < TemplateHandler
include Compilable

@@ -15,11 +39,9 @@ class ERB < TemplateHandler
self.default_format = Mime::HTML

def compile(template)
require 'erb'

magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+)\s*-?%>)/
erb = "#{magic}<% __in_erb_template=true %>#{template.source}"
::ERB.new(erb, nil, erb_trim_mode, '@output_buffer').src
Erubis.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src
end
end
end
@@ -55,7 +55,7 @@ def initialize
setup :setup_with_controller
def setup_with_controller
@controller = TestController.new
@output_buffer = ''
@output_buffer = ActionView::SafeBuffer.new
@rendered = ''

self.class.send(:include_helper_modules!)

3 comments on commit 9415935

@rubys

This comment has been minimized.

Copy link
Contributor

replied Oct 7, 2009

Perhaps a wee bit too much escaping? Look at the failures:

http://intertwingly.net/projects/AWDwR3/checkdepot.html

@crishoj

This comment has been minimized.

Copy link

replied Nov 21, 2009

Shouldn't you also use the SafeBuffer in flush_output_buffer in ActionView::Helpers::CaptureHelper?

@NZKoz

This comment has been minimized.

Copy link
Member Author

replied Nov 22, 2009

@crishoj: Possibly, can you add a ticket for me on lighthouse for that and I'll take a look after I get the next 2.3.x release pushed.

Please sign in to comment.
You can’t perform that action at this time.