output_safety.rb 6.9 KB
Newer Older
1
require 'erb'
2
require 'active_support/core_ext/kernel/singleton_class'
3 4 5

class ERB
  module Util
6
    HTML_ESCAPE = { '&' => '&amp;',  '>' => '&gt;',   '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
7
    JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
8
    HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
9
    JSON_ESCAPE_REGEXP = /[&><]/
10

11 12 13 14 15 16
    # A utility method for escaping HTML tag characters.
    # This method is also aliased as <tt>h</tt>.
    #
    # In your ERB templates, use this method to escape any unsafe content. For example:
    #   <%=h @person.name %>
    #
17
    #   puts html_escape('is a > 0 & a < 10?')
18 19 20 21 22 23
    #   # => is a &gt; 0 &amp; a &lt; 10?
    def html_escape(s)
      s = s.to_s
      if s.html_safe?
        s
      else
24
        s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe
25 26 27
      end
    end

R
R.T. Lechow 已提交
28
    # Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
29
    remove_method(:h)
30 31 32 33
    alias h html_escape

    module_function :h

34 35 36
    singleton_class.send(:remove_method, :html_escape)
    module_function :html_escape

V
Vijay Dev 已提交
37
    # A utility method for escaping HTML without affecting existing escaped entities.
38
    #
39
    #   html_escape_once('1 < 2 &amp; 3')
40 41
    #   # => "1 &lt; 2 &amp; 3"
    #
42
    #   html_escape_once('&lt;&lt; Accept & Checkout')
43 44
    #   # => "&lt;&lt; Accept &amp; Checkout"
    def html_escape_once(s)
A
Aman Gupta 已提交
45
      result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
46 47 48 49 50
      s.html_safe? ? result.html_safe : result
    end

    module_function :html_escape_once

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    # A utility method for escaping HTML entities in JSON strings. Specifically, the
    # &, > and < characters are replaced with their equivilant unicode escaped form -
    # \u0026, \u003e, and \u003c. These sequences has identical meaning as the original
    # characters inside the context of a JSON string, so assuming the input is a valid
    # and well-formed JSON value, the output will have equivilant meaning when parsed:
    # 
    #   json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
    #   # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
    # 
    #   json_escape(json)
    #   # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
    # 
    #   JSON.parse(json) == JSON.parse(json_escape(json))
    #   # => true
    # 
    # The intended use case for this method is to escape JSON strings before including
    # them inside a script tag to avoid XSS vulnerability:
    # 
    #   <script type="application/javascript">
    #     var currentUser = <%= json_escape current_user.to_json %>;
    #   </script>
    # 
    # WARNING: this helper only works with valid JSON. Using this on non-JSON values
    # will open up serious XSS vulnerabilities. For example, if you replace the
    # +current_user.to_json+ in the example above with user input instead, the browser
    # will happily eval() that string as JavaScript.
    # 
    # The escaping performed in this method is identical to those performed in the
    # ActiveSupport JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
    # set to true. Because this transformation is idempotent, this helper can be
    # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
    # 
    # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
    # is enabled, or if you are unsure where your JSON string originated from, it
    # is recommended that you always apply this helper (other libraries, such as the
    # JSON gem, does not provide this kind of protection by default; also some gems
    # might override +#to_json+ to bypass ActiveSupport's encoder).
    # 
    # The output of this helper method is marked as HTML safe so that you can directly
    # include it inside a +<script>+ tag as shown above.
    # 
    # However, it is NOT safe to use the output of this inside an HTML attribute,
    # because quotation marks are not escaped. Doing so might break your page's layout.
    # If you intend to use this inside an HTML attribute, you should use the 
    # +html_escape+ helper (or its +h+ alias) instead:
    # 
    #   <div data-user-info="<%= h current_user.to_json %>">...</div>
    # 
99
    def json_escape(s)
A
Aman Gupta 已提交
100
      result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
101
      s.html_safe? ? result.html_safe : result
102 103 104 105 106 107
    end

    module_function :json_escape
  end
end

108 109 110 111 112 113
class Object
  def html_safe?
    false
  end
end

114
class Numeric
115 116 117 118 119
  def html_safe?
    true
  end
end

120 121
module ActiveSupport #:nodoc:
  class SafeBuffer < String
A
Alexey Gaziev 已提交
122 123 124 125
    UNSAFE_STRING_METHODS = %w(
      capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
      slice squeeze strip sub succ swapcase tr tr_s upcase prepend
    )
126

127 128 129 130 131
    alias_method :original_concat, :concat
    private :original_concat

    class SafeConcatError < StandardError
      def initialize
132
        super 'Could not concatenate to the buffer because it is not html safe.'
133 134 135
      end
    end

136
    def [](*args)
A
Alexey Gaziev 已提交
137 138
      if args.size < 2
        super
139
      else
A
Alexey Gaziev 已提交
140 141 142 143 144 145 146
        if html_safe?
          new_safe_buffer = super
          new_safe_buffer.instance_eval { @html_safe = true }
          new_safe_buffer
        else
          to_str[*args]
        end
147 148 149
      end
    end

150
    def safe_concat(value)
151
      raise SafeConcatError unless html_safe?
152 153
      original_concat(value)
    end
154

155
    def initialize(*)
156
      @html_safe = true
157 158 159 160 161
      super
    end

    def initialize_copy(other)
      super
162
      @html_safe = other.html_safe?
163 164
    end

A
Akira Matsuda 已提交
165
    def clone_empty
166
      self[0, 0]
A
Akira Matsuda 已提交
167 168
    end

169
    def concat(value)
170
      if !html_safe? || value.html_safe?
171 172 173 174 175
        super(value)
      else
        super(ERB::Util.h(value))
      end
    end
176
    alias << concat
J
Joshua Peek 已提交
177

178 179 180 181
    def +(other)
      dup.concat(other)
    end

182
    def %(args)
183
      args = Array(args).map do |arg|
184 185 186 187 188 189 190 191 192 193
        if !html_safe? || arg.html_safe?
          arg
        else
          ERB::Util.h(arg)
        end
      end

      self.class.new(super(args))
    end

194
    def html_safe?
195
      defined?(@html_safe) && @html_safe
196
    end
J
Joshua Peek 已提交
197

198 199 200
    def to_s
      self
    end
201

202 203 204 205
    def to_param
      to_str
    end

206 207 208 209
    def encode_with(coder)
      coder.represent_scalar nil, to_str
    end

210
    UNSAFE_STRING_METHODS.each do |unsafe_method|
211
      if unsafe_method.respond_to?(unsafe_method)
212 213 214 215 216 217 218 219 220 221
        class_eval <<-EOT, __FILE__, __LINE__ + 1
          def #{unsafe_method}(*args, &block)       # def capitalize(*args, &block)
            to_str.#{unsafe_method}(*args, &block)  #   to_str.capitalize(*args, &block)
          end                                       # end

          def #{unsafe_method}!(*args)              # def capitalize!(*args)
            @html_safe = false                      #   @html_safe = false
            super                                   #   super
          end                                       # end
        EOT
222
      end
223
    end
224
  end
225
end
J
Joshua Peek 已提交
226

227 228 229 230
class String
  def html_safe
    ActiveSupport::SafeBuffer.new(self)
  end
231
end