Strong Parameters in Sinatra

in Ruby , Security , Sinatra

A while ago a concept called “Strong Parameters” was added to Ruby on Rails, the general idea of which was to explicitly allow or deny parameters passing into your application code.

Since most of my work right now is in Sinatra, I was interested in creating something similar with my setup in mind. I decided to give it a go using Sinatra’s conditions and the result, is my “sinatra-strong-params” Ruby gem.

Here’s an example Sinatra application using StrongParams:

 1require 'sinatra/base'
 2require 'sinatra/strong-params'
 3
 4class ExampleApp < Sinatra::Base
 5  configure do
 6    register Sinatra::StrongParams
 7  end
 8
 9  get '/', allows: [:search] do
10    # Only the 'search' parameter will make it to the execution scope.
11  end
12
13  post '/search', needs: [:search, :_csrf] do
14    # Will only ever return if both the 'search' and '_csrf' parameters are present.
15    #   Otherwise, it will raise an instance of RequiredParamMissing
16  end
17
18  error RequiredParamMissing do
19    # Handle parameter failures here.
20    [400, "No dice"]
21  end
22end

There are two new conditions we can add to any request: “allows” and “needs”. As you might have guessed, one specifies which parameters you’d like to allow through to the request handler and the other specifies which parameters must be present for the request to be handled.

Allows

Here, we simply filter out parameters to the request that are not explicitly allowed by the condition.

 1#
 2# A way to whitelist parameters.
 3#
 4#   get '/', allows: [:id, :action] do
 5#     erb :index
 6#   end
 7#
 8# Modifies the parameters available in the request scope.
 9# Stashes unmodified params in @_params
10#
11app.set(:allows) do |*passable|
12  condition do
13    unless @params.empty?
14      @_params = @_params || @params # for safety
15      globals  = settings.globally_allowed_parameters
16      passable = (globals | passable).map(&:to_sym) # make sure it's a symbol
17
18      # trim the params down
19      @params = @params.select do |param, _value|
20        passable.include?(param.to_sym)
21      end
22    end
23  end
24end

Needs

And here, we actually fail the request if one of the defined parameters is missing.

 1#
 2# A way to require parameters
 3#
 4#   get '/', needs: [:id, :action] do
 5#     erb :index
 6#   end
 7#
 8# Does not modify the parameters available to the request scope.
 9# Raises a RequiredParamMissing error if a needed param is missing
10#
11app.set(:needs) do |*needed|
12  condition do
13    if @params.nil? || @params.empty? && !needed.empty?
14      fail RequiredParamMissing, settings.missing_parameter_message
15    else
16      needed     = needed.map(&:to_sym) # make sure it's a symbol
17      sym_params = @params.dup
18
19      # symbolize the keys so we know what we're looking at
20      sym_params.keys.each do |key|
21        sym_params[(key.to_sym rescue key) || key] = sym_params.delete(key)
22      end
23
24      if needed.any? { |key| sym_params[key].nil? || sym_params[key].empty? }
25        fail RequiredParamMissing, settings.missing_parameter_message
26      end
27    end
28  end
29end

And there ya go! Lock down those parameters!