-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
output_safety.rb
228 lines (187 loc) · 6.8 KB
/
output_safety.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# frozen_string_literal: true
require "active_support/core_ext/erb/util"
require "active_support/multibyte/unicode"
class Object
def html_safe?
false
end
end
class Numeric
def html_safe?
true
end
end
module ActiveSupport # :nodoc:
class SafeBuffer < String
UNSAFE_STRING_METHODS = %w(
capitalize chomp chop delete delete_prefix delete_suffix
downcase lstrip next reverse rstrip scrub squeeze strip
succ swapcase tr tr_s unicode_normalize upcase
)
UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub)
alias_method :original_concat, :concat
private :original_concat
# Raised when ActiveSupport::SafeBuffer#safe_concat is called on unsafe buffers.
class SafeConcatError < StandardError
def initialize
super "Could not concatenate to the buffer because it is not HTML safe."
end
end
def [](*args)
if html_safe?
new_string = super
return unless new_string
string_into_safe_buffer(new_string, true)
else
to_str[*args]
end
end
alias_method :slice, :[]
def slice!(*args)
new_string = super
return new_string if !html_safe? || new_string.nil?
string_into_safe_buffer(new_string, true)
end
def chr
return super unless html_safe?
string_into_safe_buffer(super, true)
end
def safe_concat(value)
raise SafeConcatError unless html_safe?
original_concat(value)
end
def initialize(str = "")
@html_safe = true
super
end
def initialize_copy(other)
super
@html_safe = other.html_safe?
end
def concat(value)
unless value.nil?
super(implicit_html_escape_interpolated_argument(value))
end
self
end
alias << concat
def bytesplice(*args, value)
super(*args, implicit_html_escape_interpolated_argument(value))
end
def insert(index, value)
super(index, implicit_html_escape_interpolated_argument(value))
end
def prepend(value)
super(implicit_html_escape_interpolated_argument(value))
end
def replace(value)
super(implicit_html_escape_interpolated_argument(value))
end
def []=(arg1, arg2, arg3 = nil)
if arg3
super(arg1, arg2, implicit_html_escape_interpolated_argument(arg3))
else
super(arg1, implicit_html_escape_interpolated_argument(arg2))
end
end
def +(other)
dup.concat(other)
end
def *(_)
new_string = super
new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
new_safe_buffer
end
def %(args)
case args
when Hash
escaped_args = args.transform_values { |arg| explicit_html_escape_interpolated_argument(arg) }
else
escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) }
end
self.class.new(super(escaped_args))
end
attr_reader :html_safe
alias_method :html_safe?, :html_safe
remove_method :html_safe
def to_s
self
end
def to_param
to_str
end
def encode_with(coder)
coder.represent_object nil, to_str
end
UNSAFE_STRING_METHODS.each do |unsafe_method|
if unsafe_method.respond_to?(unsafe_method)
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
end
end
UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method|
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
if block # if block
to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params|
set_block_back_references(block, $~) # set_block_back_references(block, $~)
block.call(*params) # block.call(*params)
} # }
else # else
to_str.#{unsafe_method}(*args) # to_str.gsub(*args)
end # end
end # end
def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
@html_safe = false # @html_safe = false
if block # if block
super(*args) { |*params| # super(*args) { |*params|
set_block_back_references(block, $~) # set_block_back_references(block, $~)
block.call(*params) # block.call(*params)
} # }
else # else
super # super
end # end
end # end
EOT
end
private
def explicit_html_escape_interpolated_argument(arg)
(!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
end
def implicit_html_escape_interpolated_argument(arg)
if !html_safe? || arg.html_safe?
arg
else
CGI.escapeHTML(arg.to_str)
end
end
def set_block_back_references(block, match_data)
block.binding.eval("proc { |m| $~ = m }").call(match_data)
rescue ArgumentError
# Can't create binding from C level Proc
end
def string_into_safe_buffer(new_string, is_html_safe)
new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
new_safe_buffer.instance_variable_set :@html_safe, is_html_safe
new_safe_buffer
end
end
end
class String
# Marks a string as trusted safe. It will be inserted into HTML with no
# additional escaping performed. It is your responsibility to ensure that the
# string contains no malicious content. This method is equivalent to the
# +raw+ helper in views. It is recommended that you use +sanitize+ instead of
# this method. It should never be called on user input.
def html_safe
ActiveSupport::SafeBuffer.new(self)
end
end