Why don’t we validate controller parameters?

TLDR; Using strong parameters in Ruby on Rails applications to control permitted parameters is not enough. Taking care of validating allowed values is also important to make apps more secure and less error-prone. To handle the validation properly we can write custom lines of codes or use handy gems like apipie-rails.

Strong parameters have their supporters and critics. Whether you use them or not, you still should remember about validating values. params.require(:user).permit(:first_name, :last_name) is usually not enough to make Ruby on Rails application secure enough.

My reflections came from an issue that happened to me recently. I made a silly typo and it was enough to cause an avalanche of 500 errors which could have been easily avoided. A microservice I was working on made synchronous HTTP POST requests to Ruby on Rails application. One of the required parameters was class name. After receiving a request it was converted to a concrete instance of a class:

params[:class_name].constantize.find(params[:id])

It had been working as expected until, by making a typo, I provided a class name which did not exist in the application:

"User".constantize.find(1)
=> #<User id: 1, email: "[email protected]", created_at: "2018-04-20 08:52:47", updated_at: "2018-04-20 08:52:47">
"Userr".constantize.find(1)
NameError: uninitialized constant Userr
Please note, that the below line of code is an example of code injection exploitation.

From the consumer point of view 422 (Unprocessable entity) or 400 (Bad request) response status code returned by the provider would:

  1. be more correct,
  2. prevent server error causing SMS alerts,
  3. reduce my and my colleagues’ stress level 🙂

There are multiple ways to make controller actions more bulletproof. I would like to share with you two, I am familiar with.

1. Manual validation

Let’s create a simple controller to use it as an example first. For the sake of simplicity, let’s assume that there is no ActiveRecord validation on model level. To be able to test actual behaviour I created a simple Rails 5.2 application in API-only mode (rails new rooms --api).

class Api::RoomsController < ApplicationController
  def create
    Room.create!(params)
  end
end

That is not the most beautiful controller in the world, for at least two reasons:

  1. We do not know which parameters are acceptable.
  2. We do not know which values are acceptable.

I tested it quickly by making a simple cURL request:

curl -H "Content-Type: application/json" -X POST -d '{"room": { "floor": 1 } }' localhost:3000/api/rooms

And got the following response:

{"status":500,"error":"Internal Server Error","exception":"ActiveModel::ForbiddenAttributesError...

ActiveModel::ForbiddenAttributesError occurred. It is quite mysterious, though. Well, in that case, we cannot go forward without strong parameters 🙂

class Api::RoomsController < ApplicationController
  def create
    Room.create!(create_params)
  end

  private

  def create_params
    params.require(:room).permit(:owner_type, :owner_id, :floor, :price)
  end
end

After the above changes, I retried the cURL request and got the below response:

{"id":3,"floor":1,"price":null,"owner_type":null,"owner_id":null,"created_at":"2019-03-05T19:32:36.077Z","updated_at":"2019-03-05T19:32:36.077Z"}

I managed to create a new entry, but with a lot of null values which is undesired behaviour. Thanks to strong parameters we know which parameters the action accepts. Unfortunately, we still:

  1. know nothing about permitted values allowing API consumers to create valid records.
  2. have application vulnerable to code injection exploitation.

Let’s try to fix that without any external dependency:

class Api::RoomsController < ApplicationController
  class UnpermittedParameterValue < RuntimeError
    def initialize(parameter:, value:)
      @parameter = parameter
      @value = value
    end

    attr_reader :parameter, :value
  end

  rescue_from UnpermittedParameterValue, with: :invalid_parameters

  def create
    validate_create_params
    Room.create!(create_params)
  end

  private

  # Could be extracted to a dedicated class within e.g. Validation module.
  # Validation checks should support all the permitted parameters; below
  # only owner_type parameter is handled. 
  def validate_create_params 
    owner_type = create_params[:owner_type] 
    raise UnpermittedParameterValue.new(parameter: :owner_type, value: owner_type) if !Set['Company', 'Person'].include? owner_type 
    # and so on... 
  end 

  def invalid_parameters(exception) 
    render json: { errors: { exception.parameter => "'Value #{ exception.value }' is not supported value for the parameter." } }, status: 400
  end 

  def create_params 
    params.require(:room).permit(:owner_type, :owner_id, :floor, :price) 
  end 
end

Thanks to the above changes, we know that either Company or Person can be an owner of Room. Another step in the right direction. However, there are still some downsides:

  1. We need to define a dedicated validation method(s) or class(es) per action/parameter.
  2. We need to open and analyse source code to find the permitted values.

Re-running the cURL request with owner_type parameter containing a string with a typo produced the below JSON response with 400 status code:

{"errors":{"owner_type":"Value 'Peeerson' is not supported value for the parameter."}}

Better, but still not ideal. Let’s check if adding some external dependencies would make the controller better.

2. apipie-rails gem

The gem advertises itself as Ruby on Rails API documentation toolIn reality, it allows us to do more than documenting API. Thanks to its DSL, it makes validating API a cakewalk.

To make usage of it, you need to add it within application’s Gemfile and invoke rails g apipie:install afterwards. After doing so, we can apply its magic powers to the create action:

class Api::RoomsController < ApplicationController
  api :POST, "/rooms", "An end-point used for creating new rooms in the system. Standard basic auth is required."
  formats ['json']
  error 401, "Unauthorized"
  error :unprocessable_entity, "Could not create the room."
  param :room, Hash, desc: "Room details" do
    param :owner_type, ["Company", "Person"], desc: "Owner of the room", required: true
    param :owner_id, :number, desc: "ID of the room's owner", required: true
    # Below a simple ':number' validation is used. It could be extended to allow only supported floor e.g. from 1 to 12.
    param :floor, :number, desc: "Floor on which the room is located", required: true
  end
  def create
    Room.create!(create_params)
  end

  private


  def create_params
    params.require(:room).permit(:owner_type, :owner_id, :floor)
  end
end

Voilà! We have just:

  1. Documented the create action.
  2. Added quite exhaustive validation to it.
  3. (BONUS!) Get ready-to-use end-point documentation in HTML, JSON & Swagger formats for free.

Main page of API documentation generated by the gem. It presents a list of all described resources with defined descriptions.

A detailed page of API documentation generated by the gem. Thanks to it, API consumer can understand the endpoint better.

Any downsides? Yes, the gem returns 500 status response for errors like Apipie::ParamMissing and Apipie::ParamInvalid:

{"status":500,"error":"Internal Server Error","exception":"Apipie::ParamInvalid: Invalid parameter 'owner_type' value \"Peeerson\": Must be one of: 'Company', 'Person'."

If we would like to return 400 response instead, and I really would like to do so to eliminate 500 errors in case of typos, we would need to rescue the errors and handle them differently (for example in ApplicationController to make all controllers behave the same through inheritance):

class ApplicationController
  rescue_from Apipie::ParamInvalid do |exception|
    render status: 400, json: {
      status: 400,
      param: exception.param,
      value: exception.value,
      error: exception.to_s,
    }
  end
end

Thanks to the above change, we get the below response:

{"status":400,"param":"owner_type","value":"Peeerson","error":"Invalid parameter 'owner_type' value \"Peeerson\": Must be one of: 'Company', 'Person'."}

README.md of the gem is an example of very good documentation. I encourage you to take a look if you want to discover all of its possibilities.

Why is it important?

I treat application’s HTTP API endpoints like its public interface. From a consumer point of view, they should be well-documented to allow smooth integration. As soon as I need to dip deep into the source code to understand what parameters and values are allowed, a small red bulb turns on in my head.

Solid API documentation combined with proper parameters validation:

  1. reduces the number of questions from consumers, making integration(s) on HTTP layer more pleasant,
  2. makes the application more secure,
  3. makes the application more reliable,
  4. makes our work easier 🙂

To fix the issue I encountered, I improved API validation which had already been defined using apipie-rails DSL. A validation rule for the parameter value I made a typo in was set to String. I spent some time analyzing source code and replaced it by a set of allowed values: ["name_of_first_supported_class", "name_of_second_supported_class"]. Thanks to that:

  1. similar mistakes will result in more proper 400 status code,
  2. code injection is impossible.

I hope you learnt something today. There are ready-to-use tools that you can use to document and validate API end-points in Ruby on Rails application(s) you develop with minimum effort. It is worth the effort 🙂

P.S. If you handle the described issue differently, please share your thoughts in the comments.

 

Igor Springer

I build web apps. From time to time I put my thoughts on paper. I hope that some of them will be valuable for you. To teach is to learn twice.

 

Leave a Reply

Your email address will not be published. Required fields are marked *