178: Seven Security Tips
(view original Railscast)
In this episode we’ll cover seven different security issues that are common in Rails applications. Some of these have already been covered in previous episodes, but here we’ll show them all at once so that you can review your Rails applications by going though them to make sure that your application is secure.
The application we’ll be working with is a simple project management application. Users can sign up, then log in and create projects.
Each project can have an image and a number of tasks associated with it.
We’ll be using this application to demonstrate the seven security issues and our tips for solving them.
Tip 1 - Mass Assignment
We’ve put this one at the top of the list as it’s one of the most important security issues to consider. It’s a problem that can be easily overlooked and which can lead to the application’s data being compromised.
When we edit a project the form we use only has fields for the name
and photo
attributes.
When it’s submitted the ProjectsController
’s update action uses mass-assignment to update the selected project.
def update @project = current_users.projects.find(params[:id]) if @project.update_attributes(params[:project]) flash[:notice] = "Successfully updated project." redirect_to @project else render :action => 'edit' end end
All of the parameters from the form are passed as a hash to the project to update it. A user can therefore update any of the project’s attributes and even the attributes of any associated models. As a project has_many :tasks
this means that a project’s tasks can be altered.
This type of hacking by mass assignment is usually done by using a tool like curl
to send POST request to the server, but we’ll demonstrate the idea in the console.
First we’ll fetch the project with the id
of 2, a project which currently has no tasks.
>> p = Project.find(2) +----+--------+--------+--------+--------+--------+--------+--------+--------+ | id | name | cre... | upd... | pho... | pho... | pho... | pho... | use... | +----+--------+--------+--------+--------+--------+--------+--------+--------+ | 2 | Coo... | 200... | 200... | | | | | 2 | +----+--------+--------+--------+--------+--------+--------+--------+--------+ 1 row in set >> p.tasks => []
One of a project’s attributes is task_ids
, which takes an array of Task id
numbers. Given this we can use update_attributes
to change the tasks associated with the project. This way we can assign one of another project’s tasks to our project.
>> p.update_attributes(:task_ids => [4]) => true >> p.tasks +----+-----------------+-------------------+-------------------+------------+ | id | name | created_at | updated_at | project_id | +----+-----------------+-------------------+-------------------+------------+ | 4 | Sweep the yard. | 2009-09-08 20:... | 2009-09-08 20:... | 2 | +----+-----------------+-------------------+-------------------+------------+ 1 row in setThe task with the id of 4 now belongs to our project. >> p.tasks +----+-----------------+-------------------+-------------------+------------+ | id | name | created_at | updated_at | project_id | +----+-----------------+-------------------+-------------------+------------+ | 4 | Sweep the yard. | 2009-09-08 20:... | 2009-09-08 20:... | 2 | +----+-----------------+-------------------+-------------------+------------+ 1 row in set
Obviously this is a bad security flaw. By using attr_accessible
in the model, however, we can define the attributes that we want to be accessible through mass assignment.
class Project < ActiveRecord::Base belongs_to :user has_many :tasks has_attached_file :photo attr_accessible :name, :photo end
With this line in place no-one can set the task_ids
or any other attribute of a project by making a web request; only the name
and photo
can be updated. For more details about this technique watch or read episode 26.
Tip 2 - File Uploads
In our example application the users have the ability to upload a photograph for a project. We’re using Paperclip to handle the files which, by default, can be used to upload any kind of file, not just images. It happens that our application is running on the Apache web server with Passenger and that the Apache server is set up to execute PHP files too. With this setup, what happens if we upload a PHP file instead of an image?
To test this we’ll upload a PHP file that executes PHP’s phpinfo function. When run this file will display information about the server.
<?php phpinfo() ?>
When we upload the file we’ll see a broken image icon where the image should be:
But if we use the popup menu to open the image in a new tab then the file will be executed on the server.
This is a major security hole as it means that a user can run any script on the browser they want. If we’re running a Rails application on Apache and it’s configured to execute PHP or CGI scripts we need to be very careful with file upload controls.
As we’re using Paperclip to handle file uploads we can add a line like this to the relevant model file to restrict the file types that will be accepted.
validates_attachment_content_type :photo, :content_type => ['image/jpeg', 'image/png']
This ensures that only files with the content type for JPEG and PNG images are accepted. This isn’t a completely secure solution, however, as it’s possible to spoof the content type when uploading a file. For additional security the file extension should be checked to ensure that it matches only the file types we want to allow. To be extra sure we should also modify the Apache configuration file so that scripts cannot be executed from the directory to which the files are being uploaded.
Tip 3 - Filter Log Params
Our application has signup and login forms in which users can enter their username and password. By default, Rails will store all form parameters as plain text which means that when we log in, our username and password are stored in the log file.
Processing UserSessionsController#create (for 127.0.0.1 at 2009-09-10 20:52:16) [POST] Parameters: {"commit"=>"Log in", "user_session"=>{"username"=>"eifion", "password"=>"pass"}, "authenticity_token"=>"GepjvVMhxroWGfE2NX4CZTtw6wLzUyd1b+Rm88qXI5g="}
While we’ve gone to the effort of encrypting our users’ passwords in the database they are still clearly visible in the application’s log file. Fortunately this is easy to rectify. In our application’s ApplicationController
is a commented out line:
# filter_parameter_logging :password
Uncommenting this line will filter out password parameters from the log file. Other field names can be added to the list of parameters if there are other fields that need to be filtered.
Now when we log in the password is filtered out from the log file:
Processing UserSessionsController#create (for 127.0.0.1 at 2009-09-10 20:55:24) [POST] Parameters: {"commit"=>"Log in", "user_session"=>{"username"=>"eifion", "password"=>"[FILTERED]"}, "action"=>"create", "authenticity_token"=>"cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=", "controller"=>"user_sessions"}
Tip 4 - Cross-site Request Forgery Protection
This tip also revolves around the ApplicationController
, specifically this line
protect_from_forgery # :secret => '2b964d30ac961dfe405b234c10a42505'
It’s important to check that this line exists and is uncommented. It’s like this by default so should be there. This line helps protect your application from cross-site request forgery. To see how it works we’ll look at part of the source code from the edit project form.
<form action="/projects/1" class="edit_project" enctype="multipart/form-data" id="edit_project_1" method="post"> <div style="margin:0;padding:0;display:inline"> <input name="_method" type="hidden" value="put" /> <input name="authenticity_token" type="hidden" value="cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=" /> </div>
Inside the form element Rails automatically adds a hidden field called authenticity_token
. This field’s value is a key that is unique for each user’s session and is based on a random string stored in the session. Rails will automatically check this key for every single POST, PUT or DELETE request that is made. This ensures that the request is made by a user who is actually on our site, rather than by another site pretending to be that user. This token is not checked for GET requests so we need to make sure that GET requests can not change or delete our application’s data.
Tip 5 - Authorising Ownership
If we look at the page for a specific project in our application we can see that the project’s id
is in the URL. Each user has their own set of projects and we want to make sure that one user cannot view another’s projects.
Project 1 belongs to us, but if we start altering the id in the URL we can view someone else’s project.
To fix this we need to look in our ProjectsController, in particular at the code that gets the project from the database.
def show @project = Project.find(params[:id]) end
This code will fetch any project by its id
, with no authorisation to check that the project belongs to the currently logged-in user. There are a few ways we could do this, but one easy way is to use ActiveRecord associations to get the project in the scope of the current user. As a project belongs_to a user we can do this by changing the code above to
def show @project = current_user.projects.find(params[:id]) end
This will now scope the search to the projects that belong to the currently logged-in user. If we try to view another user’s project now we’ll see an error.
Note that the SQL condition in the error message has the current user’s id in the select condition. A RecordNotFound
error means that when this application runs in production mode the user will see a 404 page.
Tip 6 - SQL Injection
SQL injection is a term you’re probably familiar with so we’ll keep this tip brief.
On the projects index page we have a search form. The problem with it is that in the controller the search term that is entered is entered directly into a SQL condition.
def index @projects = current_user.projects.all(:conditions => "name like '%#{params[:search]}%'") end
This is definitely not a good thing to do as it leaves our application wide open to SQL injection attacks. All it takes is a some single quotes in the search term and a maliciously-minded user can view data they shouldn’t see, or even modify or remove data from the database.
The code in the index action finds projects scoped by the currently logged-in user, but if we enter '( or )'
as our search term we can see everyone’s projects.
Obviously this is a bad thing, so instead of passing the search term directly into the condition we should use the question mark syntax.
def index @projects = current_user.projects.all(:conditions => ["name like ?", "%#{params[:search]}%"]) end
We’re now passing the condition as an array rather than as a string. The first parameter is the search condition with the parameter replaced by a question mark, while the second parameter is the value we want to pass. Setting up the conditions this way will quote the parameter and escape any special characters in it when it’s inserted into the condition.
If we reload the page now we’ll see that our search term now returns no results.
For more information on SQL injection watch or read episode 25.
Tip 7 - HTML Injection
While writing our application we forgot to escape the HTML that displays the tasks’ names. This means that if someone enters some JavaScript between <script>
tags in the name of a task then the script will be executed.
Every time this page is reloaded the script will be executed which is simply annoying in our case, but which could be used to get sensitive data from our site and silently post it to another server.
All we need to do to solve this problem is to escape the output by wrapping anything that has been entered by a user in the h
method.
<%= h(task.name) %>
This will ensure that any HTML will be escaped and therefore any scripts entered will be displayed rather than run.
Ensuring that all user input is escaped when it’s shown on a page will prevent cross-site scripting attacks. If you want to allow certain HTML tags to be displayed the sanitize
method can be used instead of h
.
Rails 3 will automatically escape content on the page making the h
method unnecessary, but until it’s released we still need to do this in our applications.
That’s all for our list of security tips. Hopefully it gave you a good list to check your own applications against. For more information it’s well worth looking at the Ruby On Rails Security Guide which covers more topics and in greater depth than we’ve been able to show in this episode.