Skip to content

Commit 139963c

Browse files
committed
Merge branch 'master-security'
* master-security: Ensure [] respects the status of the buffer. delete vulnerable AS::SafeBuffer#[] use AS::SafeBuffer#clone_empty for flushing the output_buffer add AS::SafeBuffer#clone_empty fix output safety issue with select options Conflicts: actionpack/lib/action_view/helpers/tags/base.rb
2 parents ceb66b6 + 8ccaa34 commit 139963c

File tree

5 files changed

+63
-29
lines changed

5 files changed

+63
-29
lines changed

actionpack/lib/action_view/helpers/capture_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def with_output_buffer(buf = nil) #:nodoc:
215215
def flush_output_buffer #:nodoc:
216216
if output_buffer && !output_buffer.empty?
217217
response.body_parts << output_buffer
218-
self.output_buffer = output_buffer[0,0]
218+
self.output_buffer = output_buffer.respond_to?(:clone_empty) ? output_buffer.clone_empty : output_buffer[0, 0]
219219
nil
220220
end
221221
end

actionpack/lib/action_view/helpers/tags/base.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,11 @@ def select_content_tag(option_tags, options, html_options)
133133

134134
def add_options(option_tags, options, value = nil)
135135
if options[:include_blank]
136-
include_blank = options[:include_blank] if options[:include_blank].kind_of?(String)
137-
option_tags = content_tag(:option, include_blank, :value => '').safe_concat("\n").safe_concat(option_tags)
136+
option_tags = content_tag('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags
138137
end
139138
if value.blank? && options[:prompt]
140139
prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select')
141-
option_tags = content_tag(:option, prompt, :value => '').safe_concat("\n").safe_concat(option_tags)
140+
option_tags = content_tag('option', prompt, :value => '') + "\n" + option_tags
142141
end
143142
option_tags
144143
end

actionpack/test/template/form_options_helper_test.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def @post.to_param; 108; end
509509

510510
def test_select_under_fields_for_with_string_and_given_prompt
511511
@post = Post.new
512-
options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>"
512+
options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>".html_safe
513513

514514
output_buffer = fields_for :post, @post do |f|
515515
concat f.select(:category, options, :prompt => 'The prompt')
@@ -665,6 +665,13 @@ def test_select_with_index_option
665665
)
666666
end
667667

668+
def test_select_escapes_options
669+
assert_dom_equal(
670+
'<select id="post_title" name="post[title]">&lt;script&gt;alert(1)&lt;/script&gt;</select>',
671+
select('post', 'title', '<script>alert(1)</script>')
672+
)
673+
end
674+
668675
def test_select_with_selected_nil
669676
@post = Post.new
670677
@post.category = "<mus>"

activesupport/lib/active_support/core_ext/string/output_safety.rb

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,29 +103,41 @@ def initialize
103103
end
104104
end
105105

106-
def[](*args)
107-
new_safe_buffer = super
108-
new_safe_buffer.instance_eval { @dirty = false }
109-
new_safe_buffer
106+
def [](*args)
107+
return super if args.size < 2
108+
109+
if html_safe?
110+
new_safe_buffer = super
111+
new_safe_buffer.instance_eval { @html_safe = true }
112+
new_safe_buffer
113+
else
114+
to_str[*args]
115+
end
110116
end
111117

112118
def safe_concat(value)
113-
raise SafeConcatError if dirty?
119+
raise SafeConcatError unless html_safe?
114120
original_concat(value)
115121
end
116122

117123
def initialize(*)
118-
@dirty = false
124+
@html_safe = true
119125
super
120126
end
121127

122128
def initialize_copy(other)
123129
super
124-
@dirty = other.dirty?
130+
@html_safe = other.html_safe?
131+
end
132+
133+
def clone_empty
134+
new_safe_buffer = self[0, 0]
135+
new_safe_buffer.instance_variable_set(:@dirty, @dirty)
136+
new_safe_buffer
125137
end
126138

127139
def concat(value)
128-
if dirty? || value.html_safe?
140+
if !html_safe? || value.html_safe?
129141
super(value)
130142
else
131143
super(ERB::Util.h(value))
@@ -138,7 +150,7 @@ def +(other)
138150
end
139151

140152
def html_safe?
141-
!dirty?
153+
defined?(@html_safe) && @html_safe
142154
end
143155

144156
def to_s
@@ -161,18 +173,12 @@ def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
161173
end # end
162174
163175
def #{unsafe_method}!(*args) # def capitalize!(*args)
164-
@dirty = true # @dirty = true
176+
@html_safe = false # @html_safe = false
165177
super # super
166178
end # end
167179
EOT
168180
end
169181
end
170-
171-
protected
172-
173-
def dirty?
174-
@dirty
175-
end
176182
end
177183
end
178184

activesupport/test/safe_buffer_test.rb

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ def test_titleize
8484
assert_equal "hello&lt;&gt;", clean + @buffer
8585
end
8686

87-
test "Should concat as a normal string when dirty" do
87+
test "Should concat as a normal string when safe" do
8888
clean = "hello".html_safe
8989
@buffer.gsub!('', '<>')
9090
assert_equal "<>hello", @buffer + clean
9191
end
9292

93-
test "Should preserve dirty? status on copy" do
93+
test "Should preserve html_safe? status on copy" do
9494
@buffer.gsub!('', '<>')
9595
assert !@buffer.dup.html_safe?
9696
end
@@ -102,20 +102,42 @@ def test_titleize
102102
assert_equal "<script>", result_buffer
103103
end
104104

105-
test "Should raise an error when safe_concat is called on dirty buffers" do
105+
test "Should raise an error when safe_concat is called on unsafe buffers" do
106106
@buffer.gsub!('', '<>')
107107
assert_raise ActiveSupport::SafeBuffer::SafeConcatError do
108108
@buffer.safe_concat "BUSTED"
109109
end
110110
end
111111

112-
test "should not fail if the returned object is not a string" do
112+
test "Should not fail if the returned object is not a string" do
113113
assert_kind_of NilClass, @buffer.slice("chipchop")
114114
end
115115

116-
test "Should initialize @dirty to false for new instance when sliced" do
117-
dirty = @buffer[0,0].send(:dirty?)
118-
assert_not_nil dirty
119-
assert !dirty
116+
test "clone_empty returns an empty buffer" do
117+
assert_equal '', ActiveSupport::SafeBuffer.new('foo').clone_empty
118+
end
119+
120+
test "clone_empty keeps the original dirtyness" do
121+
assert @buffer.clone_empty.html_safe?
122+
assert !@buffer.gsub!('', '').clone_empty.html_safe?
123+
end
124+
125+
test "Should be safe when sliced if original value was safe" do
126+
new_buffer = @buffer[0,0]
127+
assert_not_nil new_buffer
128+
assert new_buffer.html_safe?, "should be safe"
129+
end
130+
131+
test "Should continue unsafe on slice" do
132+
x = 'foo'.html_safe.gsub!('f', '<script>alert("lolpwnd");</script>')
133+
134+
# calling gsub! makes the dirty flag true
135+
assert !x.html_safe?, "should not be safe"
136+
137+
# getting a slice of it
138+
y = x[0..-1]
139+
140+
# should still be unsafe
141+
assert !y.html_safe?, "should not be safe"
120142
end
121143
end

0 commit comments

Comments
 (0)