ruby-on-railsstrong-parameters

What could cause Rails to structure params as flat rather than hierarchical?


I have a strange error in my rails application that is being triggered by someone probably doing something they shouldn't be doing. But I can't figure out what they might be doing and thus how to guard against it.

The current setup is Rails 7.1.3.2, Ruby 3.3.1, though I've seen it for months including being back on Rails 7.0 with Ruby 3.2

Ultimately, I get the following error: ActionController::ParameterMissing: param is missing or the value is empty: organization

The form is relatively simple

<form action="/o" accept-charset="UTF-8" method="post">
    <input type="hidden" name="authenticity_token" value="...">
    <label for="organization_a1">A1</label>
    <input required="required" type="text" name="organization[a1]" id="organization_a1">
    <label for="organization_a2">A2</label>
    <input type="text" name="organization[email_domain]" id="organization_a2">
    <input type="submit" name="commit" value="Continue" data-disable-with="Continue">
</form>
...

Nothing fancy going on in the controller either.

def create
  @organization = Organization.create(organization_params)
  redirect_to start_home_index_path
end

def organization_params
  params.require(:organization).permit(:a1, :a2, ...)
end

The first request comes through and redirects just fine, the second fails on the params.require call

And if I look at the logs, I get the following sequence of requests.

INFO -- Started POST "/o" for 47.238.233.78 at 2024-05-14 17:37:48 +0000
Processing by OrganizationsController#create as */*
INFO -- Parameters: {"authenticity_token"=>"[FILTERED]", "organization"=>{"a1"=>"v1", "a2"=>"v2"}
, "commit"=>"Continue"}
INFO -- <successful redirect>
...
INFO -- Started POST "/o" for 47.238.233.78 at 2024-05-14 17:37:51 +0000
INFO -- Processing by OrganizationsController#create as */*
INFO -- Parameters: {"authenticity_token"=>"[FILTERED]", "organization[a1]"=>"v1", "organization[a2]"=>"v2", "commit"=>"Continue", "organization"=>{}}
INFO -- Completed 400 Bad Request in 1ms
ERROR -- ActionController::ParameterMissing (param is missing or the value is empty: organization)
...

So I can see the empty organization pointing at {} in the rails log, but if I look at the body of the request in the error, it appears as ..."organization[a1]":"v1","organization[a2]:"v2"..." with no strange empty assignments like "organization":""

You can see in the logs, this appears to be someone (same IP address) submitting the form twice within 3 seconds. The first succeeds and the second fails.

I've tried to recreate this a number of different ways, including changing the authenticity_token hidden field in the form before submitting (oddly enough, rails does not have a problem with this). Also resubmitting the same request multiple times using curl (no problem). If I change the value of the session cookie, that does cause an error with rails, but a different one (Can't verify CSRF token authenticity).

So what could cause rails to act this way? I can certainly handle this error a number of different ways, but I'd like to understand how the user is able to trigger Rails to act this way.


Solution

  • The most likely answer is that the client sent garbage in the request body.

    Parsing FormData pairs into arrays and hashes is done by Rack way before the request passes down the middleware stack to Rails.

    ActionController::Parameters is simply a hash like object that Rails uses to wrap the hashes, arrays and scalars that Rack parses out of the request body.

    If you look at the incoming request parameters in the log we can spot some big issues.

    {"authenticity_token"=>"[FILTERED]", "organization[a1]"=>"v1", "organization[a2]"=>"v2", "commit"=>"Continue", "organization"=>{}}
    

    The request body you have added to the question is very wrong.

    FormData uses = to separate between keys and values and & as the delimiter between pairs - "organization[a1]":"v1","organization[a2]:"v2" looks more like a JSON object.

    When parsing JSON brackets in object keys have no significance.

    The why is a lot harder to figure out. Could it be a malicous actor fishing for parser volunerabilies? Bad javascript that sends an XHR request but doesn't block the original event? Invalid HTML that causes the browser to try to make sense of the soup?

    If you want to replacate the issue just send the following JSON payload:

    {
      "organization[a1]": "v1",
      "organization[a2]": "v2",
      "organization": {}
    }