Rails params aren't always strings

In Rails, if you assume params[:key] is always a string, you might be making your app insecure

Written by Rico Sta. Cruz
(@rstacruz) · 29 Nov 2021

Today I came across an interesting issue in a Rails app. A simple params[:key] was throwing an error.

def index
@page = params[:page].to_i
@articles = Article.page(@page)
NoMethodError: (undefined method `to_i' for ["1"]:Array)
Did you mean? to_s

Why that happens

It turns out that while params[:something] is often assumed to be either a string or nil, but that isn’t always the case. It can also become arrays or hashes.

Terminal window
# params[:page] is a string
# params[:page] is an array
# params[:page] is a hash
Passing ?page[] or ?page[string] will automatically turn parameters to either arrays or hashes.

Security issues ahead

Whenever using params[:key], it would be wise to think “what if an array/hash is passed here?“. In this hypothetical example, the intention might be to delete one record, but it might unintentionally allow multiple deletions.

class PostController < ApplicationController
def destroy
# Delete a post with a given ID
Post.where(id: params[:id]).destroy
Terminal window
# Deletes one post
DELETE /posts/1234
# Deletes many posts...?
DELETE /posts/1234?id[]=1&id[]=2&id[]=3&...
Thankfully this won't work, because Rails has #destroy_all for collections rather than #destroy.
Hey! I write articles about web development and productivity. If you'd like to support me, subscribe to the email list so you don't miss out on updates.

Solution: strong parameters

Rails 5’s new Strong Parameters feature prevents from issues like this. Using #permit will prevent arrays and hashes from coming through.

class PostController < ApplicationController
def update
# Avoid accessing params directly like this:
# @post.update(email: params[:email])
# Instead, use #permit to specify what params
# to use:
update_params = params.permit(:email)
@post.update(email: update_params[:email])

Using permit

Using params.permit will reject hashes and arrays.

Parameters = ActionController::Parameters
Parameters.new(email: "[email protected]").permit(:email)
# => { email: "[email protected]" }
Parameters.new(email: ["ATTACK"]).permit(:email)
# => { email: nil }
Parameters.new(email: {"ATTACK" => 1}).permit(:email)
# => { email: nil }

Using require

In contrast, using params.require will only let hashes and arrays through. Using both permit and require can be used to define the shape of the expected input.

Parameters = ActionController::Parameters
# => Error:
# ActionController::ParameterMissing: param is
# missing or the value is empty: person
Parameters.new({ person: nil }).require(:person)
Parameters.new({ person: "\t" }).require(:person)
Parameters.new({ person: {} }).require(:person)
# ^ These are also errors

Written by Rico Sta. Cruz

I am a web developer helping make the world a better place through JavaScript, Ruby, and UI design. I write articles like these often. If you'd like to stay in touch, subscribe to my list.


More articles

← More articles