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 stringhttps://example.com/?page=hello
# params[:page] is an arrayhttps://example.com/?page[]=hello&page[]=world
# params[:page] is a hashhttps://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 endend
# Deletes one postDELETE /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]) endend
Using params.permit
will reject hashes and arrays.
Parameters = ActionController::Parameters
Parameters.new(email: "hi@example.com").permit(:email)# => { email: "hi@example.com" }
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
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.