Skip to content

Commit d64611b

Browse files
authored
Merge pull request rails#53193 from matthewd/query-parsing
Do more request parameter parsing ourselves
2 parents e83cfa5 + a235f20 commit d64611b

File tree

15 files changed

+432
-39
lines changed

15 files changed

+432
-39
lines changed

actionpack/lib/action_dispatch.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ class MissingController < NameError
5353
eager_autoload do
5454
autoload_under "http" do
5555
autoload :ContentSecurityPolicy
56+
autoload :InvalidParameterError, "action_dispatch/http/param_error"
57+
autoload :ParamBuilder
58+
autoload :ParamError
59+
autoload :ParameterTypeError, "action_dispatch/http/param_error"
60+
autoload :ParamsTooDeepError, "action_dispatch/http/param_error"
5661
autoload :PermissionsPolicy
62+
autoload :QueryParser
5763
autoload :Request
5864
autoload :Response
5965
end
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# frozen_string_literal: true
2+
3+
module ActionDispatch
4+
class ParamBuilder
5+
def self.make_default(param_depth_limit)
6+
new param_depth_limit
7+
end
8+
9+
attr_reader :param_depth_limit
10+
11+
def initialize(param_depth_limit)
12+
@param_depth_limit = param_depth_limit
13+
end
14+
15+
cattr_accessor :default
16+
self.default = make_default(100)
17+
18+
class << self
19+
delegate :from_query_string, :from_pairs, :from_hash, to: :default
20+
end
21+
22+
def from_query_string(qs, separator: nil, encoding_template: nil)
23+
from_pairs QueryParser.each_pair(qs, separator), encoding_template: encoding_template
24+
end
25+
26+
def from_pairs(pairs, encoding_template: nil)
27+
params = make_params
28+
29+
pairs.each do |k, v|
30+
if Hash === v
31+
v = ActionDispatch::Http::UploadedFile.new(v)
32+
end
33+
34+
store_nested_param(params, k, v, 0, encoding_template)
35+
end
36+
37+
params
38+
rescue ArgumentError => e
39+
raise InvalidParameterError, e.message, e.backtrace
40+
end
41+
42+
def from_hash(hash, encoding_template: nil)
43+
# Force encodings from encoding template
44+
hash = Request::Utils::CustomParamEncoder.encode_for_template(hash, encoding_template)
45+
46+
# Assert valid encoding
47+
Request::Utils.check_param_encoding(hash)
48+
49+
# Convert hashes to HWIA (or UploadedFile), and deep-munge nils
50+
# out of arrays
51+
hash = Request::Utils.normalize_encode_params(hash)
52+
53+
hash
54+
end
55+
56+
private
57+
def store_nested_param(params, name, v, depth, encoding_template = nil)
58+
raise ParamsTooDeepError if depth >= param_depth_limit
59+
60+
if !name
61+
# nil name, treat same as empty string (required by tests)
62+
k = after = ""
63+
elsif depth == 0
64+
# Start of parsing, don't treat [] or [ at start of string specially
65+
if start = name.index("[", 1)
66+
# Start of parameter nesting, use part before brackets as key
67+
k = name[0, start]
68+
after = name[start, name.length]
69+
else
70+
# Plain parameter with no nesting
71+
k = name
72+
after = ""
73+
end
74+
elsif name.start_with?("[]")
75+
# Array nesting
76+
k = "[]"
77+
after = name[2, name.length]
78+
elsif name.start_with?("[") && (start = name.index("]", 1))
79+
# Hash nesting, use the part inside brackets as the key
80+
k = name[1, start - 1]
81+
after = name[start + 1, name.length]
82+
else
83+
# Probably malformed input, nested but not starting with [
84+
# treat full name as key for backwards compatibility.
85+
k = name
86+
after = ""
87+
end
88+
89+
return if k.empty?
90+
91+
if depth == 0 && String === v
92+
# We have to wait until we've found the top part of the name,
93+
# because that's what the encoding template is configured with
94+
if encoding_template && (designated_encoding = encoding_template[k]) && !v.frozen?
95+
v.force_encoding(designated_encoding)
96+
end
97+
98+
# ... and we can't validate the encoding until after we've
99+
# applied any template override
100+
unless v.valid_encoding?
101+
raise InvalidParameterError, "Invalid encoding for parameter: #{v.scrub}"
102+
end
103+
end
104+
105+
if after == ""
106+
if k == "[]" && depth != 0
107+
return (v || !ActionDispatch::Request::Utils.perform_deep_munge) ? [v] : []
108+
else
109+
params[k] = v
110+
end
111+
elsif after == "["
112+
params[name] = v
113+
elsif after == "[]"
114+
params[k] ||= []
115+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
116+
params[k] << v if v || !ActionDispatch::Request::Utils.perform_deep_munge
117+
elsif after.start_with?("[]")
118+
# Recognize x[][y] (hash inside array) parameters
119+
unless after[2] == "[" && after.end_with?("]") && (child_key = after[3, after.length - 4]) && !child_key.empty? && !child_key.index("[") && !child_key.index("]")
120+
# Handle other nested array parameters
121+
child_key = after[2, after.length]
122+
end
123+
params[k] ||= []
124+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
125+
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
126+
store_nested_param(params[k].last, child_key, v, depth + 1)
127+
else
128+
params[k] << store_nested_param(make_params, child_key, v, depth + 1)
129+
end
130+
else
131+
params[k] ||= make_params
132+
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
133+
params[k] = store_nested_param(params[k], after, v, depth + 1)
134+
end
135+
136+
params
137+
end
138+
139+
def make_params
140+
ActiveSupport::HashWithIndifferentAccess.new
141+
end
142+
143+
def new_depth_limit(param_depth_limit)
144+
self.class.new @params_class, param_depth_limit
145+
end
146+
147+
def params_hash_type?(obj)
148+
Hash === obj
149+
end
150+
151+
def params_hash_has_key?(hash, key)
152+
return false if key.include?("[]")
153+
154+
key.split(/[\[\]]+/).inject(hash) do |h, part|
155+
next h if part == ""
156+
return false unless params_hash_type?(h) && h.key?(part)
157+
h[part]
158+
end
159+
160+
true
161+
end
162+
end
163+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module ActionDispatch
4+
class ParamError < ActionDispatch::Http::Parameters::ParseError
5+
def initialize(message = nil)
6+
super
7+
end
8+
9+
def self.===(other)
10+
super || (
11+
defined?(Rack::Utils::ParameterTypeError) && Rack::Utils::ParameterTypeError === other ||
12+
defined?(Rack::Utils::InvalidParameterError) && Rack::Utils::InvalidParameterError === other ||
13+
defined?(Rack::QueryParser::ParamsTooDeepError) && Rack::QueryParser::ParamsTooDeepError === other
14+
)
15+
end
16+
end
17+
18+
class ParameterTypeError < ParamError
19+
end
20+
21+
class InvalidParameterError < ParamError
22+
end
23+
24+
class ParamsTooDeepError < ParamError
25+
end
26+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require "uri"
4+
5+
module ActionDispatch
6+
class QueryParser
7+
DEFAULT_SEP = /& */n
8+
COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
9+
10+
#--
11+
# Note this departs from WHATWG's specified parsing algorithm by
12+
# giving a nil value for keys that do not use '='. Callers that need
13+
# the standard's interpretation can use `v.to_s`.
14+
def self.each_pair(s, separator = nil)
15+
return enum_for(:each_pair, s, separator) unless block_given?
16+
17+
(s || "").split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |part|
18+
next if part.empty?
19+
20+
k, v = part.split("=", 2)
21+
22+
k = URI.decode_www_form_component(k)
23+
v &&= URI.decode_www_form_component(v)
24+
25+
yield k, v
26+
end
27+
28+
nil
29+
end
30+
end
31+
end

actionpack/lib/action_dispatch/http/request.rb

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def self.empty
6363

6464
def initialize(env)
6565
super
66+
67+
@rack_request = Rack::Request.new(env)
68+
6669
@method = nil
6770
@request_method = nil
6871
@remote_ip = nil
@@ -71,6 +74,8 @@ def initialize(env)
7174
@ip = nil
7275
end
7376

77+
attr_reader :rack_request
78+
7479
def commit_cookie_jar! # :nodoc:
7580
end
7681

@@ -388,34 +393,67 @@ def session_options=(options)
388393
# Override Rack's GET method to support indifferent access.
389394
def GET
390395
fetch_header("action_dispatch.request.query_parameters") do |k|
391-
rack_query_params = super || {}
392-
controller = path_parameters[:controller]
393-
action = path_parameters[:action]
394-
rack_query_params = Request::Utils.set_binary_encoding(self, rack_query_params, controller, action)
395-
# Check for non UTF-8 parameter values, which would cause errors later
396-
Request::Utils.check_param_encoding(rack_query_params)
397-
set_header k, Request::Utils.normalize_encode_params(rack_query_params)
396+
encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
397+
rack_query_params = ActionDispatch::ParamBuilder.from_query_string(rack_request.query_string, encoding_template: encoding_template)
398+
399+
set_header k, rack_query_params
398400
end
399-
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError => e
401+
rescue ActionDispatch::ParamError => e
400402
raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
401403
end
402404
alias :query_parameters :GET
403405

404406
# Override Rack's POST method to support indifferent access.
405407
def POST
406408
fetch_header("action_dispatch.request.request_parameters") do
407-
pr = parse_formatted_parameters(params_parsers) do |params|
408-
super || {}
409+
encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
410+
411+
param_list = nil
412+
pr = parse_formatted_parameters(params_parsers) do
413+
if param_list = request_parameters_list
414+
ActionDispatch::ParamBuilder.from_pairs(param_list, encoding_template: encoding_template)
415+
else
416+
# We're not using a version of Rack that provides raw form
417+
# pairs; we must use its hash (and thus post-process it below).
418+
fallback_request_parameters
419+
end
420+
end
421+
422+
# If the request body was parsed by a custom parser like JSON
423+
# (and thus the above block was not run), we need to
424+
# post-process the result hash.
425+
if param_list.nil?
426+
pr = ActionDispatch::ParamBuilder.from_hash(pr, encoding_template: encoding_template)
409427
end
410-
pr = Request::Utils.set_binary_encoding(self, pr, path_parameters[:controller], path_parameters[:action])
411-
Request::Utils.check_param_encoding(pr)
412-
self.request_parameters = Request::Utils.normalize_encode_params(pr)
428+
429+
self.request_parameters = pr
413430
end
414-
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError, EOFError => e
431+
rescue ActionDispatch::ParamError, EOFError => e
415432
raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
416433
end
417434
alias :request_parameters :POST
418435

436+
def request_parameters_list
437+
# We don't use Rack's parse result, but we must call it so Rack
438+
# can populate the rack.request.* keys we need.
439+
rack_post = rack_request.POST
440+
441+
if form_pairs = get_header("rack.request.form_pairs")
442+
# Multipart
443+
form_pairs
444+
elsif form_vars = get_header("rack.request.form_vars")
445+
# URL-encoded
446+
ActionDispatch::QueryParser.each_pair(form_vars)
447+
elsif rack_post && !rack_post.empty?
448+
# It was multipart, but Rack did not preserve a pair list
449+
# (probably too old). Flat parameter list is not available.
450+
nil
451+
else
452+
# No request body, or not a format Rack knows
453+
[]
454+
end
455+
end
456+
419457
# Returns the authorization header regardless of whether it was specified
420458
# directly or through one of the proxy alternatives.
421459
def authorization
@@ -492,6 +530,10 @@ def reset_stream(body_stream)
492530
yield
493531
end
494532
end
533+
534+
def fallback_request_parameters
535+
rack_request.POST
536+
end
495537
end
496538
end
497539

actionpack/lib/action_dispatch/request/utils.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ def self.handle_array(params)
8383
end
8484

8585
class CustomParamEncoder # :nodoc:
86-
def self.encode(request, params, controller, action)
87-
return params unless controller && controller.valid_encoding? && encoding_template = action_encoding_template(request, controller, action)
86+
def self.encode_for_template(params, encoding_template)
87+
return params unless encoding_template
8888
params.except(:controller, :action).each do |key, value|
8989
ActionDispatch::Request::Utils.each_param_value(value) do |param|
9090
# If `param` is frozen, it comes from the router defaults
@@ -98,8 +98,14 @@ def self.encode(request, params, controller, action)
9898
params
9999
end
100100

101+
def self.encode(request, params, controller, action)
102+
encoding_template = action_encoding_template(request, controller, action)
103+
encode_for_template(params, encoding_template)
104+
end
105+
101106
def self.action_encoding_template(request, controller, action) # :nodoc:
102-
request.controller_class_for(controller).action_encoding_template(action)
107+
controller && controller.valid_encoding? &&
108+
request.controller_class_for(controller).action_encoding_template(action)
103109
rescue MissingController
104110
nil
105111
end

0 commit comments

Comments
 (0)