Gotcha: Rails params aren't always strings
In Rails, if you assume params[:key] is always a string, you might be making your app insecure!
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)
end
NoMethodError: (undefined method `to_i' for ["1"]:Array)
Did you mean? to_s
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.
# 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
?page[]
or ?page[string]
will automatically turn parameters to either arrays or hashes.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
end
end
# Deletes one post
DELETE /posts/1234
# Deletes many posts...?
DELETE /posts/1234?id[]=1&id[]=2&id[]=3&...
#destroy_all
for collections rather than #destroy
.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])
end
end
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 }
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
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