The Ruby on Rails _json Juggling Attack
Luke Jahnke9 December 2024

Ruby on Rails is a web framework which mostly provides access to user-provided data via the params object. The object is an instance of ActionController::Parameters, which can be used similar to a Hash - a collection of key-value pairs in Ruby. The contents include values from the request body, the query string, and for routes such as get "/users/:user_id", the path.

# Introducing the _json juggling attack

The _json juggling attack is an in-band signaling attack targeting JSON parsing within Ruby on Rails. The vulnerability exists in code that handles the conflict between the following:

The code responsible for JSON parsing and resolving this conflict can be found inside actionpack/lib/action_dispatch/http/parameters.rb and looks as follows:

DEFAULT_PARSERS = {
  Mime[:json].symbol => -> (raw_post) {
    data = ActiveSupport::JSON.decode(raw_post)
    data.is_a?(Hash) ? data : { _json: data }
  }
}

We see that to maintain the Hash-like nature of params, a hash is created with a key of _json that references our not-a-Hash object. The critical thing to notice about the above code is the not what is there, but what is missing. There is nothing preventing the supply of a JSON object with a _json key. Supplying a key of the string "_json" will be functionally equivalent to the Ruby symbol :_json (from the above code snippet), as parameters are stored as a HashWithIndifferentAccess.

For a simple example of how this enables a vulnerability, consider a Rails application that has a delete item endpoint. The code handles both deleting a single item and deleting multiple items. The single item JSON is {"id": 123} and the multi-item JSON is [456, 789]. The _json juggling attack is where an attacker submits JSON structured to be acceptable in both cases at the same time:

{
  "id": 123,
  "_json": [456, 789]
}

An authorisation bypass vulnerability then results when an application's authorisation code is inconsistent with the action execution code. For example, the authorisation code preferencing the multi-item form and the action execution code preferencing the single-item form.

# Rails Parameter Testing Setup

The following Dockerfile can be used to create a Rails application that sends the applications view of params back in the response.

FROM ruby:3.3

RUN gem install rails:7.1.3.3

RUN rails new param_dumper --minimal --api --skip-active-record

WORKDIR param_dumper/

RUN cat <<"EOF" > route_create_template.rb
  route "root 'dump#index', via: :all"
  route "match '/users/:user_id', to: 'dump#index', via: :all"
EOF

RUN ./bin/rails app:template LOCATION=route_create_template.rb

RUN cat <<"EOF" > app/controllers/dump_controller.rb
class DumpController < ApplicationController
  def index
    render plain: params.inspect
  end
end
EOF

ENTRYPOINT ["./bin/rails", "server", "--binding=0.0.0.0"]

It can be used as follows after placing the above in a file named Dockerfile:

$ docker build -t rails-param-dumper .

$ docker run --name rails -p 127.0.0.1:3000:3000 -d --rm rails-param-dumper

$ curl -d 'b=frombody' http://127.0.0.1:3000/users/frompath?q=fromquery
#<ActionController::Parameters {
  "b"=>"frombody",
  "q"=>"fromquery",
  "controller"=>"dump",
  "action"=>"index",
  "user_id"=>"frompath"
} permitted: false>

$ docker stop rails

If you wish to send a JSON request with curl, do not forget to set an appropriate Content-Type header. This can be achieved with a command such as curl -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/

# Bonus questions about params

There are a number of other interesting security-related questions that can be asked:

Q: What is the precedence order of the three sources?
A: Path is highest, then query string, followed by request body.

Q: What is the permitted attribute in ActionController::Parameters?
A: This is used to protect against https://brakemanscanner.org/docs/warning_types/mass_assignment/ attacks.

Q: Can you clobber the values of controller or action that are included in params?
A: I tried and was unsuccessful :(

Q: Are there any query strings that cannot be parsed into params?
A: Yes, examples can be found in Rack's test code, such as x[y]=1&x[y]z=2 and x[y]=1&x[y][][w]=2. These can be useful to fingerprint the usage of Rack. It used to be possible to use a&a[], as covered in blog post by bjeanes in 2010.

« Back to homepage