Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.
This repository was archived by the owner on Aug 12, 2026. It is now read-only.

Double-unescaping of deepObject query parameter values corrupts URI-encoded characters #25

Description

@cwaider

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions