Skip to content

Commit be543e8

Browse files
committed
allow :file to be outside rails root, but anything else must be inside the rails view directory
Conflicts: actionpack/test/controller/render_test.rb actionview/lib/action_view/template/resolver.rb CVE-2016-0752
1 parent 5875bc3 commit be543e8

File tree

9 files changed

+93
-16
lines changed

9 files changed

+93
-16
lines changed

actionpack/lib/abstract_controller/rendering.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ def view_assigns
7777
# render "foo/bar" to render :file => "foo/bar".
7878
# :api: plugin
7979
def _normalize_args(action=nil, options={})
80-
if action.is_a? Hash
80+
case action
81+
when ActionController::Parameters
82+
unless action.permitted?
83+
raise ArgumentError, "render parameters are not permitted"
84+
end
85+
action
86+
when Hash
8187
action
8288
else
8389
options

actionpack/test/controller/render_test.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ def conditional_hello_with_record
5252
end
5353
end
5454

55+
def dynamic_render
56+
render params[:id] # => String, AC:Params
57+
end
58+
59+
def dynamic_render_with_file
60+
# This is extremely bad, but should be possible to do.
61+
file = params[:id] # => String, AC:Params
62+
render file: file
63+
end
64+
5565
def conditional_hello_with_public_header
5666
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
5767
render :action => 'hello_world'
@@ -251,6 +261,27 @@ def accessing_logger_in_template
251261
class ExpiresInRenderTest < ActionController::TestCase
252262
tests TestController
253263

264+
def test_dynamic_render_with_file
265+
# This is extremely bad, but should be possible to do.
266+
assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
267+
response = get :dynamic_render_with_file, { id: '../\\../test/abstract_unit.rb' }
268+
assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')),
269+
response.body
270+
end
271+
272+
def test_dynamic_render
273+
assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
274+
assert_raises ActionView::MissingTemplate do
275+
get :dynamic_render, { id: '../\\../test/abstract_unit.rb' }
276+
end
277+
end
278+
279+
def test_dynamic_render_file_hash
280+
assert_raises ArgumentError do
281+
get :dynamic_render, { id: { file: '../\\../test/abstract_unit.rb' } }
282+
end
283+
end
284+
254285
def test_expires_in_header
255286
get :conditional_hello_with_expires_in
256287
assert_equal "max-age=60, private", @response.headers["Cache-Control"]

actionview/lib/action_view/lookup_context.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def find(name, prefixes = [], partial = false, keys = [], options = {})
125125
end
126126
alias :find_template :find
127127

128+
def find_file(name, prefixes = [], partial = false, keys = [], options = {})
129+
@view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
130+
end
131+
128132
def find_all(name, prefixes = [], partial = false, keys = [], options = {})
129133
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
130134
end

actionview/lib/action_view/path_set.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,35 @@ def find(*args)
4646
find_all(*args).first || raise(MissingTemplate.new(self, *args))
4747
end
4848

49+
def find_file(path, prefixes = [], *args)
50+
_find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
51+
end
52+
4953
def find_all(path, prefixes = [], *args)
54+
_find_all path, prefixes, args, false
55+
end
56+
57+
def exists?(path, prefixes, *args)
58+
find_all(path, prefixes, *args).any?
59+
end
60+
61+
private
62+
63+
def _find_all(path, prefixes, args, outside_app)
5064
prefixes = [prefixes] if String === prefixes
5165
prefixes.each do |prefix|
5266
paths.each do |resolver|
53-
templates = resolver.find_all(path, prefix, *args)
67+
if outside_app
68+
templates = resolver.find_all_anywhere(path, prefix, *args)
69+
else
70+
templates = resolver.find_all(path, prefix, *args)
71+
end
5472
return templates unless templates.empty?
5573
end
5674
end
5775
[]
5876
end
5977

60-
def exists?(path, prefixes, *args)
61-
find_all(path, prefixes, *args).any?
62-
end
63-
64-
private
65-
6678
def typecast(paths)
6779
paths.map do |path|
6880
case path

actionview/lib/action_view/renderer/abstract_renderer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module ActionView
1515
# that new object is called in turn. This abstracts the setup and rendering
1616
# into a separate classes for partials and templates.
1717
class AbstractRenderer #:nodoc:
18-
delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
18+
delegate :find_template, :find_file, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
1919

2020
def initialize(lookup_context)
2121
@lookup_context = lookup_context

actionview/lib/action_view/renderer/template_renderer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def determine_template(options) #:nodoc:
3030
elsif options.key?(:html)
3131
Template::HTML.new(options[:html], formats.first)
3232
elsif options.key?(:file)
33-
with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
33+
with_fallbacks { find_file(options[:file], nil, false, keys, @details) }
3434
elsif options.key?(:inline)
3535
handler = Template.handler_for_extension(options[:type] || "erb")
3636
Template.new(options[:inline], "inline template", handler, :locals => keys)

actionview/lib/action_view/template/resolver.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,13 @@ def clear_cache
112112
# Normalizes the arguments and passes it on to find_templates.
113113
def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
114114
cached(key, [name, prefix, partial], details, locals) do
115-
find_templates(name, prefix, partial, details)
115+
find_templates(name, prefix, partial, details, false)
116+
end
117+
end
118+
119+
def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[])
120+
cached(key, [name, prefix, partial], details, locals) do
121+
find_templates(name, prefix, partial, details, true)
116122
end
117123
end
118124

@@ -173,15 +179,16 @@ def initialize(pattern=nil)
173179

174180
private
175181

176-
def find_templates(name, prefix, partial, details)
182+
def find_templates(name, prefix, partial, details, outside_app_allowed = false)
177183
path = Path.build(name, prefix, partial)
178-
query(path, details, details[:formats])
184+
query(path, details, details[:formats], outside_app_allowed)
179185
end
180186

181-
def query(path, details, formats)
187+
def query(path, details, formats, outside_app_allowed)
182188
query = build_query(path, details)
183189

184190
template_paths = find_template_paths query
191+
template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
185192

186193
template_paths.map { |template|
187194
handler, format, variant = extract_handler_and_format_and_variant(template, formats)
@@ -196,6 +203,10 @@ def query(path, details, formats)
196203
}
197204
end
198205

206+
def reject_files_external_to_app(files)
207+
files.reject { |filename| !inside_path?(@path, filename) }
208+
end
209+
199210
if RUBY_VERSION >= '2.2.0'
200211
def find_template_paths(query)
201212
Dir[query].reject { |filename|
@@ -216,6 +227,12 @@ def find_template_paths(query)
216227
end
217228
end
218229

230+
def inside_path?(path, filename)
231+
filename = File.expand_path(filename)
232+
path = File.join(path, '')
233+
filename.start_with?(path)
234+
end
235+
219236
# Helper for building query glob string based on resolver's pattern.
220237
def build_query(path, details)
221238
query = @pattern.dup

actionview/lib/action_view/testing/resolvers.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def to_s
1919

2020
private
2121

22-
def query(path, exts, formats)
22+
def query(path, exts, formats, _)
2323
query = ""
2424
EXTENSIONS.each_key do |ext|
2525
query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)'
@@ -44,7 +44,7 @@ def query(path, exts, formats)
4444
end
4545

4646
class NullResolver < PathResolver
47-
def query(path, exts, formats)
47+
def query(path, exts, formats, _)
4848
handler, format, variant = extract_handler_and_format_and_variant(path, formats)
4949
[ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format, :variant => variant)]
5050
end

actionview/test/template/render_test.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ def test_render_partial_from_default
142142
assert_equal "only partial", @view.render("test/partial_only")
143143
end
144144

145+
def test_render_outside_path
146+
assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
147+
assert_raises ActionView::MissingTemplate do
148+
@view.render(:template => "../\\../test/abstract_unit.rb")
149+
end
150+
end
151+
145152
def test_render_partial
146153
assert_equal "only partial", @view.render(:partial => "test/partial_only")
147154
end

0 commit comments

Comments
 (0)