Version: openapi_parameters 0.12.0
When a query parameter uses style: deepObject and its value contains characters that were percent-encoded (e.g. + encoded as %2B), the value gets unescaped twice. The second unescape pass corrupts the decoded value because Rack::Utils.unescape treats + as a space, so a correctly decoded + from the first pass becomes a space in the second.
This causes OpenAPI request validation to fail with a 400 for valid inputs. In our case, date-time values with UTC offsets (e.g. 2026-01-01T00:00:00.000+00:00) are rejected because the + in the offset is turned into a space, producing the invalid string 2026-01-01T00:00:00.000 00:00.
Steps to reproduce
Given an OpenAPI parameter defined as:
- in: query
name: filter
style: deepObject
explode: true
schema:
type: object
properties:
from:
type: string
format: date-time
And a request with the correctly percent-encoded query string:
GET /resource?filter[from]=2026-01-01T00%3A00%3A00.000%2B00%3A00
The value received by the application is "2026-01-01T00:00:00.000 00:00" instead of "2026-01-01T00:00:00.000+00:00".
Root cause
In Query#parse_query, Rack::Utils.parse_query is called with a block that calls Rack::Utils.unescape, which decodes percent-encoded characters including %2B → +. The returned hash therefore already contains fully decoded values.
For deepObject parameters, parse_deep_object then calls explode_value, which calls Rack::Utils.unescape a second time on those already-decoded values:
# lib/openapi_parameters/query.rb
def parse_query(query_string)
Rack::Utils.parse_query(query_string) do |s|
Rack::Utils.unescape(s) # first unescape: %2B → +
end
end
def explode_value(value, parameter, is_array)
value = Array(value).map! { |v| Rack::Utils.unescape(v) } # second unescape: + → " "
...
end
Rack::Utils.unescape (which delegates to URI.decode_www_form_component) treats + as a space. So a + that was correctly produced by the first unescape pass becomes a space in the second, silently corrupting the value.
Non-deepObject parameters are not affected because they bypass explode_value.
Expected behaviour
deepObject parameter values should be decoded exactly once. The second Rack::Utils.unescape call in explode_value should either be removed or the values in parse_deep_object should be taken from the raw (not yet decoded) query string.
Environment
openapi_parameters 0.12.0
rack 3.2.6
- Ruby 4.0.5
Version:
openapi_parameters0.12.0When a query parameter uses
style: deepObjectand its value contains characters that were percent-encoded (e.g.+encoded as%2B), the value gets unescaped twice. The second unescape pass corrupts the decoded value becauseRack::Utils.unescapetreats+as a space, so a correctly decoded+from the first pass becomes a space in the second.This causes OpenAPI request validation to fail with a 400 for valid inputs. In our case,
date-timevalues with UTC offsets (e.g.2026-01-01T00:00:00.000+00:00) are rejected because the+in the offset is turned into a space, producing the invalid string2026-01-01T00:00:00.000 00:00.Steps to reproduce
Given an OpenAPI parameter defined as:
And a request with the correctly percent-encoded query string:
The value received by the application is
"2026-01-01T00:00:00.000 00:00"instead of"2026-01-01T00:00:00.000+00:00".Root cause
In
Query#parse_query,Rack::Utils.parse_queryis called with a block that callsRack::Utils.unescape, which decodes percent-encoded characters including%2B→+. The returned hash therefore already contains fully decoded values.For
deepObjectparameters,parse_deep_objectthen callsexplode_value, which callsRack::Utils.unescapea second time on those already-decoded values:Rack::Utils.unescape(which delegates toURI.decode_www_form_component) treats+as a space. So a+that was correctly produced by the first unescape pass becomes a space in the second, silently corrupting the value.Non-
deepObjectparameters are not affected because they bypassexplode_value.Expected behaviour
deepObjectparameter values should be decoded exactly once. The secondRack::Utils.unescapecall inexplode_valueshould either be removed or the values inparse_deep_objectshould be taken from the raw (not yet decoded) query string.Environment
openapi_parameters0.12.0rack3.2.6