
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index

AVOID SESSION FIXATION ATTACKS 433
The attacker finds a way of attracting people who use the target.domain application to his own form. Those people will probably have cookies from target.domain in their browser. If these people submit the attacker’s form, the content of the hidden field is sent to the echo server on target.domain’s port 7. The echo server dutifully echos this back to the browser. If the browser decides to display the returned data as HTML (some versions of Internet Explorer do), it will execute the JavaScript code. Because the originating domain is target.domain the session cookie is made available to the script.
This isn’t really a Rails development issue; it works on the client side. However, to reduce the probability of a successful attack on your application, you should deactivate any echo services on your web servers. This alone does not provide full security, as there are also other services (such as FTP and POP3) that can also be used instead of the echo server.
21.3 Avoid Session Fixation Attacks
If you know someone’s session id, then you could create HTTP requests that use it. When Rails receives those requests, it thinks they’re associated with the original user, and so will let you do whatever that user can do.
Rails goes a long way towards preventing people from guessing other people’s session ids, as it constructs these ids using a secure hash function. In effect they’re very large random numbers. However, there are ways of achieving almost the same effect.
In a session fixation attack, the bad guy gets a valid session id from our application, then passes this on to a third party in such a way that the third party will use this same session. If that person uses the session to log in to our application, the bad guy, who also has access to that session id, will also be logged in.4
A couple of techniques help eliminate session fixation attacks. First, you might find it helpful to keep the IP address of the request that created the session in the session data. If this changes, you can cancel the session. This will penalize users who move their laptops across networks and home users whose IP addresses change when PPPOE leases expire.
4Session fixation attacks are described in great detail in a document from ACROS Secu-
rity, available at http://www.secinf.net/uplarticle/11/session_fixation.pdf.
Prepared exclusively for Rida Al Barazi
Report erratum

CREATING RECORDS DIRECTLY FROM FORM PARAMETERS 434
Second, you should consider creating a new session every time someone logs in. That way the legimate user will continue with their use of the application while the bad guy will be left with an orphaned session id.
21.4 Creating Records Directly from Form Parameters
Let’s say you want to implement a user registration system. Your users table looks like this.
create table users ( |
|
|
|
id |
integer primary key, |
|
|
name |
varchar(20) |
not null, |
|
password |
varchar(20) |
not null, |
|
role |
varchar(20) |
not null |
default "user", |
approved integer |
not null |
default 0 |
|
); |
|
|
|
create unique index users_name_unique on users(name);
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 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.
Prepared exclusively for Rida Al Barazi
Report erratum

DON’T TRUST ID PARAMETERS 435
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
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 afraid that you might forget to apply attr_protected( ) to the right attributes before making your model available 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, as 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
21.5 Don’t Trust ID Parameters
When we first discussed retrieving data, we introduced the find( ) method, which retrieved a row based on its primary key value. This method takes an optional hash parameter, which can be used to impose additional constraints on the rows returned.
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.
Prepared exclusively for Rida Al Barazi
Report erratum

DON’T EXPOSE CONTROLLER METHODS 436
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.
def show |
|
|
id |
= |
params[:id] |
user_id = |
session[:user_id] || -1 |
|
@order |
= |
Order.find(id, :conditions => [ "user_id = ?", user_id]) |
rescue |
|
|
redirect_to :action => "index" end
This problem 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. Unfortunately, neither delete( ) nor destroy( ) supports additional :conditions parameters. You’ll need to do the checking yourself, either by first reading the row to check ownership or by constructing an SQL where clause and passing it to delete_all( ) or destroy_all( ).
Another solution to this issue is to use associations in your application. If we declare that a user has_many orders, then we can constrain the search to find only orders for that user with code such as
user.orders.find(params[:id])
21.6 Don’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.
Sometimes an action is used as a helper, but is never intended to be invoked directly by the end user. For example, the e-mail program might display a list showing the subject lines of all the mail for a particular user. Next to each entry in the list is a Read E-Mail button. These buttons link back to actions using a URL such as
http://website.domain/email/read/1357
In this URL, the string 1357 is the id of the e-mail to be read.
Prepared exclusively for Rida Al Barazi
Report erratum

DON’T EXPOSE CONTROLLER METHODS 437
When you design this type of application, it’s easy to forget that the read( ) method is publicly exposed. In your mind, the only way that read( ) gets called is when a user clicks the link from the list of e-mails.
However, an adventurous user might have a look at the URL and wonder what would happen if they typed it in manually, giving different numbers at the end. Unless your application was written with security in mind, it’s perfectly possible that these users will be able to read other people’s e-mail.
An incorrect implementation of the read( ) action would be
def read
@email = Email.find(params[:id]) end
This method returns an e-mail given an id, regardless of the e-mail’s owner. One possible solution is to add a test for ownership.
def read
@email = Email.find(params[:id])
unless @email.owner_id == session[:user_id] flash[:notice] = "E-Mail not found" redirect_to(:action => "index")
end end
(Notice how the error message is deliberately nonspecific; had we said, “This e-mail belongs to someone else,” we’re giving away information that we really shouldn’t be sharing.)
Even better than testing in the controller is to delegate the checking to the model. This way, we can arrange things so that we never even read someone else’s e-mail into memory. Our action method would become
def read
@email = Email.find_by_id_and_user(params[:id], session[:user_id]) unless @email
flash[:notice] = "E-Mail not found" redirect_to(:action => "index")
end end
This uses a dynamically generated finder method that returns an e-mail by id only if it also belongs to the current user.
Remember that all your public actions can be invoked directly from a browser or by using hand-crafted HTML. Make sure these methods verify access rights if required.
Prepared exclusively for Rida Al Barazi
Report erratum

FILE UPLOADS 438
21.7 File Uploads
Some community-oriented web sites allow their participants to upload files for other participants to download. Unless you’re careful, these uploaded files could be used to attack your site.
For example, imagine someone uploading a file whose name ended with
.rhtml or .cgi (or any other extension associated with executable content on your site). If you link directly to these files on the download page, when the file is selected your webserver might be tempted to execute its contents, rather than simply download it. This would allow an attacker to run arbitrary code on your server.
The solution is never to allow users to upload files that are subsequently made accessible directly to other users. Instead, upload files into a directory that is not accessible to your web server (outside the DocumentRoot in Apache terms). Then provide a Rails action that allows people to view these files. Within this action, be sure that you
•Validate that the name in the request is a simple, valid filename matching an existing file in the directory or row in the table. Do not accept filenames such as ../../etc/passwd (see the sidebar Input Validation Is Difficult). You might even want to store uploaded files in a database table and use ids, rather than names, to refer to them.
•When you download a file that will be displayed in a browser, be sure to escape any HTML sequences it contains to eliminate the potential for XSS attacks. If you allow the downloading of binary files, make sure you set the appropriate Content-type HTTP header to ensure that the file will not be displayed in the browser accidentally.
The descriptions starting on page 297 describe how to download files from a Rails application, and the section on uploading files starting on page 350 shows an example that uploads image files into a database table and provides an action to display them.
21.8 Don’t Cache Authenticated Pages
Remember that page caching bypasses any security filters in your application. Use action or fragment caching if you need to control access based on session information. See Section 16.8, Caching, Part One, on page 318, and Section 17.10, Caching, Part Two, on page 366, for more information.
Prepared exclusively for Rida Al Barazi
Report erratum