|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +# Because we implement our own query string parsing, and it is extremely |
| 4 | +# similar to Rack's in most simple cases, it would be very easy to have |
| 5 | +# locations sneak in where we unknowingly depend on Rack's |
| 6 | +# interpretation rather than our own, potentially creating edge-case |
| 7 | +# incompatibilities. |
| 8 | +# |
| 9 | +# To counter that, we monkey-patch Rack in our tests to allow-list the |
| 10 | +# call sites that are known to be safe & appropriate. |
| 11 | +# |
| 12 | +# This file may need to change in response to future Rack changes. If |
| 13 | +# you're here because you're adding new code/tests to Rails, though, you |
| 14 | +# probably need to work out how to ensure you're using the Rails query |
| 15 | +# parser instead. |
| 16 | +module RackParsingOverride |
| 17 | + UnexpectedCall = Class.new(Exception) |
| 18 | + |
| 19 | + # The only expected calls to Rack::QueryParser#parse_nested_query are |
| 20 | + # from Rack::Request#GET/POST, which are separately protected below. |
| 21 | + module ParserPatch |
| 22 | + def parse_nested_query(*) |
| 23 | + unless caller_locations.any? { |loc| loc.path == __FILE__ && (loc.lineno == RackParsingOverride::GET_LINE || loc.lineno == RackParsingOverride::POST_LINE) } |
| 24 | + raise UnexpectedCall, "Unexpected call to Rack::QueryParser#parse_nested_query" |
| 25 | + end |
| 26 | + super |
| 27 | + end |
| 28 | + end |
| 29 | + |
| 30 | + # This is where we do the real checking, because we need to catch |
| 31 | + # every caller that might _use_ the cached result of Rack's parsing, |
| 32 | + # not just the first call site where parsing gets triggered. |
| 33 | + module RequestPatch |
| 34 | + # Single list of permitted callers -- we don't care about GET vs POST |
| 35 | + def self.permitted_caller? |
| 36 | + caller_locations.any? do |loc| |
| 37 | + # Our parser calls Rack's to prepopulate caches |
| 38 | + loc.path.end_with?("lib/action_dispatch/http/request.rb") && loc.label == "request_parameters_list" || |
| 39 | + # and as a fallback for older Rack versions |
| 40 | + loc.path.end_with?("lib/action_dispatch/http/request.rb") && loc.label == "fallback_request_parameters" || |
| 41 | + # This specifically tests that a "pure" Rack middleware |
| 42 | + # doesn't interfere with our parsing |
| 43 | + (loc.path.end_with?("test/dispatch/request/query_string_parsing_test.rb") && loc.label == "populate_rack_cache") || |
| 44 | + # Rack::MethodOverride obviously uses Rack's parsing, and |
| 45 | + # that's fine: it's looking for a simple top-level key. |
| 46 | + # Checking for a specific internal method is fragile, but we |
| 47 | + # don't want to ignore any app that happens to have |
| 48 | + # MethodOverride on its call stack! |
| 49 | + (loc.path.end_with?("lib/rack/method_override.rb") && loc.label == "method_override_param") |
| 50 | + end |
| 51 | + end |
| 52 | + |
| 53 | + def params |
| 54 | + unless RequestPatch.permitted_caller? |
| 55 | + raise UnexpectedCall, "Unexpected call to Rack::Request#params" |
| 56 | + end |
| 57 | + super |
| 58 | + end |
| 59 | + ::RackParsingOverride::PARAMS_LINE = __LINE__ - 2 |
| 60 | + |
| 61 | + def GET |
| 62 | + unless RequestPatch.permitted_caller? |
| 63 | + raise UnexpectedCall, "Unexpected call to Rack::Request#GET" |
| 64 | + end |
| 65 | + super |
| 66 | + end |
| 67 | + ::RackParsingOverride::GET_LINE = __LINE__ - 2 |
| 68 | + |
| 69 | + def POST |
| 70 | + unless RequestPatch.permitted_caller? |
| 71 | + raise UnexpectedCall, "Unexpected call to Rack::Request#POST" |
| 72 | + end |
| 73 | + super |
| 74 | + end |
| 75 | + ::RackParsingOverride::POST_LINE = __LINE__ - 2 |
| 76 | + end |
| 77 | + |
| 78 | + Rack::QueryParser.class_eval do |
| 79 | + # Being careful here, as this is more internal |
| 80 | + unless method_defined?(:parse_nested_query) |
| 81 | + raise "Rack changed? Can't patch absent Rack::QueryParser#parse_nested_query" |
| 82 | + end |
| 83 | + prepend ParserPatch |
| 84 | + end |
| 85 | + |
| 86 | + Rack::Request.prepend RequestPatch |
| 87 | +end |
0 commit comments