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.
params
get "/users/:user_id"
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.
_json
"_json"
:_json
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}
[456, 789]
{ "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.
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:
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/
curl
Content-Type
curl -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/
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.
permitted
ActionController::Parameters
Q: Can you clobber the values of controller or action that are included in params? A: I tried and was unsuccessful :(
controller
action
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.
x[y]=1&x[y]z=2
x[y]=1&x[y][][w]=2
a&a[]