Permalink
Browse files

Make Padrino::Rendering use ActiveSupport::SafeBuffer

This change switches all rendering to use SafeBuffer instead of a raw string.

SafeBuffer is a Subclass of String, so concats and instantiations are cheap.

Concatenating SafeBuffer with a normal String escapes the String. Concatenating
SafeBuffer with another SafeBuffer keeps both intact.

This change requires to extensions: a special template implementation for
Erubis and a port of the XssMods used in HAML. At least the latter can be
dropped once HAML makes them available as a standalone.

All strings returned from #render are considered escaped. Strings can be
marked safe for concatenation using String#html_safe, which turns returns the
String as a SafeBuffer.
  • Loading branch information...
skade committed Jan 28, 2013
1 parent 2aa0ef8 commit 2c89e1fc7b70c4906b291b3781908c02a0d1233e
@@ -25,17 +25,29 @@ class TemplateNotFound < RuntimeError
] unless defined?(IGNORE_FILE_PATTERN)
##
# Default rendering options used in the #render-method.
# Default options used in the #resolve_template-method.
#
DEFAULT_RENDERING_OPTIONS = { :strict_format => false, :raise_exceptions => true } unless defined?(DEFAULT_RENDERING_OPTIONS)
class << self
##
# Default engine configurations for Padrino::Rendering
#
# @return {Hash<Symbol,Hash>}
# The configurations, keyed by engine.
def engine_configurations
@engine_configurations ||= {}
end
##
# Main class that register this extension.
#
def registered(app)
app.send(:include, InstanceMethods)
app.extend(ClassMethods)
engine_configurations.each do |engine, configs|
app.set engine, configs
end
end
alias :included :registered
end
@@ -189,7 +201,7 @@ def render(engine, data=nil, options={}, locals={}, &block)
# Cleanup the template
@current_engine, engine_was = engine, @current_engine
@_out_buf, _buf_was = "", @_out_buf
@_out_buf, _buf_was = ActiveSupport::SafeBuffer.new, @_out_buf
# Pass arguments to Sinatra render method
super(engine, data, options.dup, locals, &block)
@@ -290,3 +302,6 @@ def locale
end # InstanceMethods
end # Rendering
end # Padrino
require 'padrino-core/application/rendering/extensions/haml'
require 'padrino-core/application/rendering/extensions/erubis'
@@ -0,0 +1,55 @@
begin
require 'erubis'
module Padrino
module Erubis
##
# SafeBufferEnhancer is an Erubis Enhancer that compiles templates that
# are fit for using ActiveSupport::SafeBuffer as a Buffer.
#
# @api private
module SafeBufferEnhancer
def add_expr_literal(src, code)
src << " #{@bufvar}.concat (" << code << ').to_s;'
end
def add_expr_escaped(src, code)
src << " #{@bufvar}.safe_concat " << code << ';'
end
def add_text(src, text)
src << " #{@bufvar}.safe_concat '" << escape_text(text) << "';" unless text.empty?
end
end
##
# SafeBufferTemplate is the classic Erubis template, augmented with
# SafeBufferEnhancer.
#
# @api private
class SafeBufferTemplate < ::Erubis::Eruby
include SafeBufferEnhancer
end
##
# Modded ErubisTemplate that doesn't insist in an String as output
# buffer.
#
# @api private
class Template < Tilt::ErubisTemplate
def precompiled_preamble(locals)
old_postamble = super.split("\n")[0..-2]
[old_postamble, "#{@outvar} = _buf = (#{@outvar} || ActiveSupport::SafeBuffer.new)"].join("\n")
end
end
end
end
Tilt.prefer Padrino::Erubis::Template, :erb
if defined? Padrino::Rendering
Padrino::Rendering.engine_configurations[:erb] =
{:engine_class => Padrino::Erubis::SafeBufferTemplate}
end
rescue LoadError
end
@@ -0,0 +1,124 @@
begin
require 'haml'
# This is a copy of the HAML projects XssMods that can only be
# required when using rails.
#
# Remove after the HAML project fixed that issue.
module Haml
module Helpers
# This module overrides Haml helpers to work properly
# in the context of ActionView.
# Currently it's only used for modifying the helpers
# to work with Rails' XSS protection methods.
module XssMods
def self.included(base)
%w[html_escape find_and_preserve preserve list_of surround
precede succeed capture_haml haml_concat haml_indent
haml_tag escape_once].each do |name|
base.send(:alias_method, "#{name}_without_haml_xss", name)
base.send(:alias_method, name, "#{name}_with_haml_xss")
end
end
# Don't escape text that's already safe,
# output is always HTML safe
def html_escape_with_haml_xss(text)
str = text.to_s
return text if str.html_safe?
Haml::Util.html_safe(html_escape_without_haml_xss(str))
end
# Output is always HTML safe
def find_and_preserve_with_haml_xss(*args, &block)
Haml::Util.html_safe(find_and_preserve_without_haml_xss(*args, &block))
end
# Output is always HTML safe
def preserve_with_haml_xss(*args, &block)
Haml::Util.html_safe(preserve_without_haml_xss(*args, &block))
end
# Output is always HTML safe
def list_of_with_haml_xss(*args, &block)
Haml::Util.html_safe(list_of_without_haml_xss(*args, &block))
end
# Input is escaped, output is always HTML safe
def surround_with_haml_xss(front, back = front, &block)
Haml::Util.html_safe(
surround_without_haml_xss(
haml_xss_html_escape(front),
haml_xss_html_escape(back),
&block))
end
# Input is escaped, output is always HTML safe
def precede_with_haml_xss(str, &block)
Haml::Util.html_safe(precede_without_haml_xss(haml_xss_html_escape(str), &block))
end
# Input is escaped, output is always HTML safe
def succeed_with_haml_xss(str, &block)
Haml::Util.html_safe(succeed_without_haml_xss(haml_xss_html_escape(str), &block))
end
# Output is always HTML safe
def capture_haml_with_haml_xss(*args, &block)
Haml::Util.html_safe(capture_haml_without_haml_xss(*args, &block))
end
# Input is escaped
def haml_concat_with_haml_xss(text = "")
raw = instance_variable_defined?('@_haml_concat_raw') ? @_haml_concat_raw : false
haml_concat_without_haml_xss(raw ? text : haml_xss_html_escape(text))
end
# Output is always HTML safe
def haml_indent_with_haml_xss
Haml::Util.html_safe(haml_indent_without_haml_xss)
end
# Input is escaped, haml_concat'ed output is always HTML safe
def haml_tag_with_haml_xss(name, *rest, &block)
name = haml_xss_html_escape(name.to_s)
rest.unshift(haml_xss_html_escape(rest.shift.to_s)) unless [Symbol, Hash, NilClass].any? {|t| rest.first.is_a? t}
with_raw_haml_concat {haml_tag_without_haml_xss(name, *rest, &block)}
end
# Output is always HTML safe
def escape_once_with_haml_xss(*args)
Haml::Util.html_safe(escape_once_without_haml_xss(*args))
end
private
# Escapes the HTML in the text if and only if
# Rails XSS protection is enabled *and* the `:escape_html` option is set.
def haml_xss_html_escape(text)
return text unless Haml::Util.rails_xss_safe? && haml_buffer.options[:escape_html]
html_escape(text)
end
end
class ErrorReturn
# Any attempt to treat ErrorReturn as a string should cause it to blow up.
alias_method :html_safe, :to_s
alias_method :html_safe?, :to_s
alias_method :html_safe!, :to_s
end
end
end
module Haml
module Helpers
include XssMods
end
end
if defined? Padrino::Rendering
Padrino::Rendering.engine_configurations[:haml] =
{:escape_html => true}
end
rescue LoadError
end
@@ -9,6 +9,7 @@
require 'active_support/core_ext/array/extract_options' # extract_options
require 'active_support/inflector/methods' # constantize
require 'active_support/inflector/inflections' # pluralize
require 'active_support/core_ext/string/output_safety' # SafeBuffer and html_safe
require 'active_support/inflections' # load default inflections
require 'yaml' unless defined?(YAML) # load yaml for i18n
require 'win32console' if RUBY_PLATFORM =~ /(win|m)32/ # ruby color support for win
@@ -1,5 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + '/helper')
require 'i18n'
require 'slim'
describe "Rendering" do
def setup
@@ -465,6 +466,39 @@ def is; "IS."; end
assert_equal 'THIS. IS. SPARTA!', body
end
should 'render erb to a SafeBuffer' do
mock_app do
get '/' do
render :erb, '<p><%= "<script lang=\"ronin\">alert(\"https://github.com/ronin-ruby/ronin\")</script>" %></p>'
end
end
get '/'
assert ok?
assert_equal '<p>&lt;script lang=&quot;ronin&quot;&gt;alert(&quot;https://github.com/ronin-ruby/ronin&quot;)&lt;/script&gt;</p>', body
end
should 'render haml to a SafeBuffer' do
mock_app do
get '/' do
render :haml, '%p= %s{<script lang="ronin">alert("https://github.com/ronin-ruby/ronin")</script>}'
end
end
get '/'
assert ok?
assert_equal '<p>&lt;script lang=&quot;ronin&quot;&gt;alert(&quot;https://github.com/ronin-ruby/ronin&quot;)&lt;/script&gt;</p>', body.strip
end
should 'render slim to a SafeBuffer' do
mock_app do
get '/' do
render :slim, 'p = %s{<script lang="ronin">alert("https://github.com/ronin-ruby/ronin")</script>}'
end
end
get '/'
assert ok?
assert_equal '<p>&lt;script lang=&quot;ronin&quot;&gt;alert(&quot;https://github.com/ronin-ruby/ronin&quot;)&lt;/script&gt;</p>', body.strip
end
should 'renders hashes and arrays as json' do
mock_app do
get '/hash' do

0 comments on commit 2c89e1f

Please sign in to comment.