When most people think about building a web application, they think about the design, programming, debugging, testing, and all the work that has to happen before an application goes live. Writing the code—while, of course, critical—isn’t the only major technology puzzle that has to get solved before an application runs. Bringing that application to the public (or even to an intranet) requires a few more critical steps that are as much about system administration as about code. The Rails framework approach is quite different from the usual CGI or PHP approach, so there are a fair number of Rails-specific issues you need to address.
First, you need to be prepared to battle the hostile nature of the Web. Every publicly exposed application will be tested and tried by a variety of visitors that you may not want or like, and even private applications sometimes face challenges from users. Using Rails isn’t particularly dangerous, and a lot of key techniques for protecting your applications from harm have already been covered. Nonetheless, it’s worth reviewing some Rails features that can be especially helpful.
It’s best to consider your application’s security before deploying it rather than after. It’s much easier to test for security leaks in the relative privacy of development mode, when only the schedule is a likely obstacle, rather than in a publicly available installation with real users who will miss the service if it goes away for a while.
The biggest risk for any application is data coming from outside of your own trusted self. As most applications invite users to put data in, this is an extremely common issue. You shouldn’t, of course, seal your application off from outside data—instead, you should always treat the data with suspicion, only trusting it after inspecting it carefully or keeping it in forms where even the worst attacks can cause no harm.
In Rails application building, this means that you should never ever trust any data in parameters (or any other data you didn’t write by hand yourself into the code), as it likely comes from outside of your system. There are many different angles where this can be an issue.
Most applications built on databases rely on Structured Query Language (SQL) to get data in and out. Even if you’re relying on Rails to handle all of your database interactions, Rails still uses SQL to communicate with the database.
To understand what’s going on, start with a simple find
call:
email = params[:email] User.find(:first, :conditions => "email = '#{email}'")
If params[:email]
contains
"[email protected]"
, then the
substitution in the double-quoted string used as a value for :conditions
will produce:
email = '[email protected]'
Using that, Rails executes a SQL statement against the database, like:
SELECT * FROM users WHERE (email = '[email protected]') LIMIT 1
That works nicely. However, imagine if params[:email]
contained "' OR 1) --"
. Rails would then
execute:
SELECT * FROM users WHERE (email = '' OR 1) -- ') LIMIT 1
The --
is a SQL comment, causing everything after it to be ignored.
Suddenly, an attacker can
retrieve all of the email addresses stored in your database. To avoid
this and other serious scenarios, change the way that you pass
conditions to a query:
User.find(:first, :conditions => ["email = ?", email])
Passing the :conditions
to Rails using the array notation will, as noted at the
end of Chapter 4,
tells Rails to sanitize the value of email
before including it in the query. If
you’re letting Rails create your SQL, that should be all you need to
do. If, however, you cannot avoid creating SQL yourself, as you need
to use find_by_sql
, then use
the model’s quote
method to do the
necessary escaping. That might look like:
User.find_by_sql "SELECT * FROM users WHERE email = '#{User.quote(email)}'"
It’s always best to explicitly ask Rails to check on parameters you don’t trust.
Cross-site scripting lets users attack other users. The attacking user enters potentially harmful HTML into your system, which is then displayed directly back to other users.
For instance, look at this code from a view:
<p>Email address: <%= user.email %></p>
This works perfectly well if the email
address is innocent. But what if a user set his address to be <script
src='http://example.org/my_evil_script.js'>
? When the
page is displayed, anybody could fetch and execute the remote
JavaScript, which could perform any number of unpleasant
functions.
To avoid this you should always escape content on output, as in:
<p>Email address: <%=h
user.email %></p>
Of course, your models should always do the tightest possible
validation, which would have noticed that the script
element wasn’t an address. For plain
text fields, however, there’s not a lot more you can do with
validation.
To help you remember and test this, there’s a handy plug-in you
can use called safe_erb
(http://agilewebdevelopment.com/plugins/safe_erb). It
treats any data coming from parameters, the database, or files on the
disk as suspect until it’s untainted by means of the h
escaping in views. Instead of displaying the suspect
data, the safe_erb
plug-in will
cause your program to display an error. You will develop good habits
very quickly when using this!
There are a few other options for dealing with markup in parameters. Sometimes you do need to display
HTML in the output, to allow people to use certain tags such as
<b>
or <i>
in content, for example. Rail’s
sanitize
helper lets
you control exactly what can get through or not—allow <b>
and <i>
but forbid <script>
, for instance. Other times
you just want to strip all markup and only keep the plain text. Then,
you probably want to use strip_tags
, which
removes all HTML tags.
CSRF is an attack often used for gaining unauthorized access to password-protected websites. If an attacker can persuade you to log into a web app (pretty much everybody leaves themselves logged in all the time these days anyway) and then visit their web page with a malignant JavaScript, that script can fake a form POST to the web app using your existing session and cause destructive behavior.
The way to avoid this problem is to have every form include a special token that guarantees to the application that the incoming POST was in response to a page that came from that web app specifically. Fortunately, Rails has this protection built-in. Open up app/controllers/application.rb and you’ll see:
# Uncomment the :secret if you're not using the cookie session store
protect_from_forgery #
:secret => '468e1168cef232cb93c2d56919e9fe4f
You should follow the comment and remove the highlighted
#
if you’re using the database or filesystem for your
sessions.
If you view the source of a form generated from Rails, you’ll see the magic token:
<form action="/people" class="new_person" id="new_person" method="post"><div style="margin:0;padding:0"><input name="authenticity_token" type="hidden"
value="f80a01b9f14d38e0816877e832637e3cc9e668a1" />
</div>
One thing to note here is that if you’re generating your own forms, rather than relying on Rails’ help, you’ll still need to include the hidden form parameter manually. This can especially be a concern when Ajax code on the browser is generating requests.
Predictable environments such as Rails leave themselves open to
people being able to read more than they ought to. This can be very
convenient in development mode, but creates major problems when
applications are exposed to real users. Consider the following
controller, where show_secret_stuff
gives logged-in users (as set up in Chapter 14) a place to see their secret
stuff:
def show_secret_stuff @user = current_user @secret = Secrets.find(params[:id]) end
Typically, the secret
objects
will have sequential IDs in the database, like 5, 6, 7, etc. We
assume that the user will land on this page from
a list of possible URLs generated in another page, like:
<% @user.secrets.each do |secret| %> <%= link_to secret.name, :action => 'show_secret_stuff', :id => secret %> <% end %>
However, there’s nothing to stop them from incrementing that
id
in the URL to explore other
people’s secrets, too. The controller just shown didn’t take any steps
to make sure the secrets actually belonged to the current user. The
answer is to ensure your finders are always as specific as possible.
In our case the controller should instead say:
def show_secret_stuff
@user = current_user
@secret = @user.secrets.find(params[:id])
end
Getting the secrets through the @user
object rather than directly will
ensure that users can’t see other users’ secrets. The most important
time to watch out for this is always when you’re doing resource
nesting—i.e., showing anything that’s part of a has_many
relationship. Never assume that
your users are friendly, and always check as much as possible before
letting a controller’s action do its work.
Security is never complete. For a lot more information on potential vulnerabilities, check the Ruby on Rails Security Project at http://www.rorsecurity.info/ regularly. The other key point to remember, though, is that your code and Rails itself are only two layers of a complex system that makes your application possible. Attackers can assault flaws in your operating system, your web server, or your database, not just your application. Always keep an eye out for security notices and updates, and ensure your operating system stays up-to-date.