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.

example.rb
def index
@page = params[:page].to_i
@articles = Article.page(@page)
end
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
https://example.com/?page=hello
# params[:page] is an array
https://example.com/?page[]=hello&page[]=world
# params[:page] is a hash
https://example.com/?page[a]=hello&page[b]=world
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.

post_controller.rb
class PostController < ApplicationController
def destroy
# Delete a post with a given ID
Post.where(id: params[:id]).destroy
end
end
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.

post_controller.rb
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])
end
end

Using permit

Using params.permit will reject hashes and arrays.

example.rb
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.

example.rb
Parameters = ActionController::Parameters
Parameters.new({}).require(:person)
# => 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.

Comments

More articles

← More articles