Secure web programming in Ruby on Rails

by Jeffrey S. Foster, Department of Computer Science, University of Maryland, College Park

www.cs.umd.edu/~jfoster

Key:

Creating a Rails project

If you haven’t already, you need to install Ruby and Rails. Then:

$ rails new talks
$ cd talks; ls
Gemfile		app		doc		script
Gemfile.lock	config		lib		test
README		config.ru	log		tmp
Rakefile	db		public		vendor

Here are some key pieces:

We’ll learn more about these pieces later. Next, let’s start the server running

$ script/rails server
> http://localhost:3000

You can also write s instead of server, and you can run just rails instead of script/rails (assuming rails is in your path). Since there’s no application code, the server only gives you access to file in public/, and the root of the server fethces public/index.html by default. (You must remove that file if you want to redirect the root path elsewhere.)

Scaffolding

If you want to do things that follow “the Rails way,” it is often very easy to do a lot with few commands. Let’s start by creating a basic model, view, and controller to let us work with a talk. To start out, we’ll just include a few fields in talks

$ rails generate scaffold talk title:text speaker:string start_time:datetime

As you can see from the outpt of this command, it created a big mess of files; those files provide a lot of default behaviors. First, let’s see what new web requests the generated scaffoling can handle; the connection between web requests and Rails methods are called routes.

$ rake routes
    talks GET    /talks(.:format)          {:action=>"index", :controller=>"talks"}
          POST   /talks(.:format)          {:action=>"create", :controller=>"talks"}
 new_talk GET    /talks/new(.:format)      {:action=>"new", :controller=>"talks"}
edit_talk GET    /talks/:id/edit(.:format) {:action=>"edit", :controller=>"talks"}
     talk GET    /talks/:id(.:format)      {:action=>"show", :controller=>"talks"}
          PUT    /talks/:id(.:format)      {:action=>"update", :controller=>"talks"}
          DELETE /talks/:id(.:format)      {:action=>"destroy", :controller=>"talks"}

From left to right, this table lists

  1. The internal Rails name for a path/URL

  2. An HTTP “verb” are path for a request

    • Note that Rails has standard workarouns in case some of these verbs aren’t implemented by a particualr browser.

    • The :id is a pattern variable standing for a database row id number

    • The (.:format) part allows non-HTML requests

  3. The name of the controller and method (or action) that will handle this particular request.

This is sometimes called RESTful (“representational state transfer”) routing, and provides a standard mapping between HTTP verbs and particular actions taken. It makes the most sense when there is a resource that needs this set of operations. Rails does allow you to customize this set easily, adding and removing particular actions. You can also create arbitrary mappings between particular URLs and actions. See Rails Routing from the Outside In.

Rails organization

Now let’s look in some detail when a particular request comes in and gets routed by Rails. Suppose the user goes to

> http://localhost:3000/talks

From the routing table, we can see that a GET request to this URL goes to the talks controller and its index action. Based on the controller names, Rails knows to look at a particular file: app/controllers/talks_controller.rb. Notice this is “convention over configuration”; the talks_controller.rb file must define a class called TalksController. Looking in this file, we see that the scaffolding includes a default method for each of the actions above. Let’s take a look at the index method:

# GET /talks
# GET /talks.json
def index
  @talks = Talk.all

  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @talks }
  end
end

The first thing we see is setting the field @talks to Talk.all. Where did Talk come from? It’s a model that was also created by the scaffoling:

$ cat app/models/talk.rb
class Talk < ActiveRecord::Base
end

This class looks like it does nothing, but in fact inheriting from ActiveRecord::Base gives Talk a huge amount of functionality. For example, calling Talk.all returns a collection of all the rows of the talks tables. Particular Talk instances also have methods for each database column. For example, if t is a Talk, then one can call t.title to get the talk title, or t.title=“Foo” to set the talk title. (Yes, this is another nice example of convention over configuration.)

Going back to the controller, we see that @talks is set to all talks. Then assume we’re asking to render HTML (the format.html case), we do…nothing? Well, not exactly. Take a look at app/views/talks/index.html.erb. This is a view, which in this case is HTML with embedded Ruby code inside special tags:

<% Ruby code evaluated for its side effect only %>
<%= Ruby code whose result is inlined as a string in the HTML %>

In the particular code in .../index.html.erb, we see that we iterate through @talks and, for each element (a Talk instance), we make an HTML table row filled in appropriately. (Notice also that there’s some magic going on here: @talks was a field of TalksController, but it’s being accessed outside of that class somehow. It’s all done with smoke and mirrors…)

Also, notice that the connection between the index action and the index.html.erb view was again “convention over configuration”.

Before going into too much detail on the view, let’s try this all out and see if it works

> localhost:3000/talks

Uh oh. We get the error Could not find table ‘talks’. We forgot to make the database!

By the way, it’s worth it at this point to remember that log/ directory, which records everything that happens on the server, including errors: For example,

$ cat log/development.log

will let you see this particular error again.

Creating a database and migrations

We can confirm that our database is actually completely empty:

$ ls -l db/
-rw-r--r--  1 jfoster  jfoster    0 May 12 17:08 development.sqlite3
drwxr-xr-x  3 jfoster  jfoster  102 May 13 13:41 migrate
-rw-r--r--  1 jfoster  jfoster  343 May 12 17:07 seeds.rb

By default, Rails uses sqlite3 for databases, but it’s easy to change this to MySQL or even other databases, if you want. The database configuration is stored in config/database.yml (comments removed):

development:
  adapter: sqlite3
  database: db/development.sqlite3
  pool: 5
  timeout: 5000

test:
  adapter: sqlite3
  database: db/test.sqlite3
  pool: 5
  timeout: 5000

production:
  adapter: sqlite3
  database: db/production.sqlite3
  pool: 5
  timeout: 5000

The development database is what Rails will use by default. The test database is, no surprise, used if you Test your application. The production database is used if you run in production mode, which you can do with

RAILS_ENV=production script/rails s

You can also add other environments beyond these three as you like.

To actually create the database, we need to tell Rails what to put in it. Fortunately, creating the scaffolding autgenerated a migration for us:

$ cat db/migrate/..._create_talks.rb
class CreateTalks < ActiveRecord::Migration
  def change
    create_table :talks do |t|
      t.text :title
      t.string :speaker
      t.datetime :start_time

      t.timestamps
    end
  end
end

The change method tells Rails how to modify the current database. We can invoke this migration with

$ rake db:migrate # go forward

Rails puts some extra tables into the database to track which migrations have happened, and you can rollback migrations as needed

$ rake db:rollback # go backward
$ rake db:migrate

Note: Hand-written migrations will often use up and down methods, which are called for roll-forward and rollback, respectively.

In addition to modifying development.sqlite3, the migration also updated Rails’s database schema file

$ cat db/schema.rb
# Some text omitted
create_table "talks", :force => true do |t|
  t.text     "title"
  t.string   "speaker"
  t.datetime "start_time"
  t.datetime "created_at"
  t.datetime "updated_at"
end

Notice that Rails automatically included some extra bookkeeping fields in the database, created_at and updated_at. It will manage these fields automatically, as well. Tip: If you want to examine or tweak the database manually, The SQLite Manager plugin for Firefox is very convenient.

Now that the database exists, we can use the web server.

> http://localhost:3000/talks
> http://localhost:3000/talks/new
> Create Talk
> http://localhost:3000/talks/1
> etc...

One thing that is not shown in schema.rb is the id field that is, by default, added to all models in Rails. These ids are integers that start at 1 and are used to uniquely identify database rows. They are not reused even if you later delete a row (though wraparound is theoretically possible).

Very basic routing

Recall that the connections between URLs and actions are called routes. These routes are specified in a configuration file:

$ cat config/routes.rb
Talks::Application.routes.draw do
  resources :talks
  # lots of comments elided
end

Since talks have a standard set of RESTful routes, all we need is the single line above in the routing file. (Also, notice that although it doesn’t really look like it, this is just Ruby code.) Let’s edit this file to include the line

root :to => 'talks#index'

so that going to the root of the server returns the list of talks. Then we just need to do

$ rm public/index.html

(Otherwise the root route is ignored.)

Views

Now that we see what the web server looks like, let’s look at the views, which are in app/views/talks/*.html.erb. Looking through these files, there are a few things of note.

First, we use link_to methods to create links, and we name particular pages using the names from the Rails routes. (Try rake routes again.) The named routes can either be listed with a _path suffixes, which gives a relative path to the page, or an +url suffix, to get an absolute path. Just as the URLs for the routes are parameterizes, the route _paths or _urls are also parameterized, e.g., edit_talk_path(talk). For the show action, you just pass talk.

The scaffolding-generated views also include the _partial view_ _form, which is called from both new.html.erb and edit.html.erb with the code

<%= render 'form' %>

You can read more about partial views in Layouts and Rendering in Rails.

The _form.html.erb file also has some code at the top for reporting errors. In particular, Rails lets you add validations to models to enforce certain requirements on table rows. See Active Record Validations and Callbacks.

Finally, notice that the form begins with

<%= form_for(@talk) do |f| %>

This helper method generates the right HTML form tags for Rails. Inside the code block passed to form_for, we can use f to refer to the form, e.g., f.text_area and f.submit. The calls such as f.label :title transform symbol names into capitalized English text using a set of rules. You can override the standard translation by editing config/locales/en.yml. (You can also store translations for other languages in config/locales.)

SQL Injection

See slides.

Make some (minor) improvements

Just for fun, let’s make two minor improvements. First, suppose that in the list of all talks, we want to list talks by start time. We can do this by modifying TalksController#index to do the sorting:

@talks = Talk.all.sort { |a,b| a.start_time <=> b.start_time }

Of course, we could also have made the same call inside the corresponding view, index.html.erb. In general, it’s not always obvious in Rails where code should go. However, it’s generally harder to read .html.erb files than to read .rb files (since the former have lots of things besides Ruby code in them), so in this case we opted for the controller.

Next, let’s add an end_time field, so that talks can have a length. We need to write a migration that makes the correct calls. Fortunately, Rails includes another generator to create an appropriate migration with little effort

$ rails g migration AddEndTimeToTalks end_time:datetime

(Here using g instead of generate is sufficient for Rails to know what we mean.) Here is the generated migration:

class AddEndTimeToTalks < ActiveRecord::Migration
  def change
    add_column :talks, :end_time, :datetime
  end
end

We can see that add_column is called to specify the table name, the new column name, and its type.

Suppose that after we create this migration, we’d like to also make another change: we originally made the speaker column a string, which is text of a fixed size. Perhaps that is a mistake, so let’s change that to be a text field instead by manually adding the following line to AddEndTimeToTalks:

change_column :talks, :speaker, :text

Now let’s run the migration

$ rake db:migrate

And just for run, we can try to undo it

$ rake db:rollback # oops, doesn't work!

This particular migration is irreversible because we added the change from string to text; Rails knows that it cannot always convert from text to string because the text may be too long to fit.

Note that after we make this change, the scaffolding code that was generated is not automatically updated. In particular, we need to edit three views for a talk: index.html.erb, _form.html.erb, and show.html.erb. Interestingly, we don’t need to edit TalksController.

Mass assignment vulnerability and Security concerns with adding users

See slides.

Setting up devise

Devise is a gem. In Rails 3, gem management is easy: we just add needed gems to Gemfile in the root directory of the project. In this case, we modify Gemfile in include

gem 'devise'

Now we need to install the new gem, which we do with

$ bundle install # downloads and installs any newly needed gems

At this point, we actually need to restart Rails to make the new gem available. But for the moment, hold off on that as there are a few other things we need to do to set up devise:

$ rails generate devise:install
# be sure to follow printed instructions
$ rails generate devise user
$ rake db:migrate
# restart Rails now

Now we can look at what devise added to our database model:

$ cat db/schema.rb
create_table "users", :force => true do |t|
  t.string   "email",                  :default => "", :null => false
  t.string   "encrypted_password",     :default => "", :null => false
  t.string   "reset_password_token"
  t.datetime "reset_password_sent_at"
  t.datetime "remember_created_at"
  t.integer  "sign_in_count",          :default => 0
  t.datetime "current_sign_in_at"
  t.datetime "last_sign_in_at"
  t.string   "current_sign_in_ip"
  t.string   "last_sign_in_ip"
  t.datetime "created_at"
  t.datetime "updated_at"
end

add_index "users", ["email"], :name => "index_users_on_email", :unique => true
add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true

Clearly devise implements a lot of logic! The purposes of most of these fields should be obvious. For this particular demo we’ll mostly leave the user model alone, but it’s generally safe to add fields to it, e.g., a user name, affiliation, address, etc. In these cases we’ll need to edit the views devise uses to include our new fields. We copy the devise views into the project for editing with

$ rake devise:views  # so we can edit the devise views

You may also want to look at the routes devise adds. Among others, it includes routes for signing in, out, for user registration, and for password creation and update.

Note that the text devise displays can be found in config/locales/devise.en.yml. There are several awkward wording choices that you may want to fix up for your particular web site. (We’ll leave that alone for the demo.)

Devise also includes additional functionality that we can activate if desired. In particular, we can require that email addresses are confirmed by users before an account is activated. To do this, just edit app/models/user.rb to include

devise ..., :confirmable

We also need to update the database model to include the fields for email configmration so

$ rake db:rollback
# uncomment confirmable fields in devise migration
$ rake db:migrate

Finally, there are two more things to do. As of this writing, there’s a bug in devise in which one of the views refers to an incorrect path Edit app/views/devise/mailers/confirmation_instructions.html.erb so that confirmation_url is replaced with user_confirmation_url. And, since we are sending mail (or at least, pretending to), we need to make the following configuration addition:

# config/environments/development.rb
config.action_mailer.default_url_options = { :host => 'localhost:3000' }

Be sure to restart Rails after making these changes.

Layouts

Like many sites, we want the devise user state to be available on most every web page. In other words, we should show the current user’s email address and a “log out” link if a user is logged in, and otherwise we should show “login” and “register” links.

To place something on every view, we can use a layout, which is just a container for other views. Take a look at app/views/layouts/application.html.erb. This is the default layout that’s rendered for everything. Notice that at the top, it has the standard HTML headers, plus some magic incantations to include stylesheets and Javascript. it also has something about CSRF, which we’ll discuss later.

Inside of the layout, there’s a call to yield, which occurs at the point where the nested view should be rendered. For example, if we render index.html.erb, Rails will start by rendering application.html.erb; then at the yield, it will start rendering index.html.erb; and when that finishes rendering, it will resume rendering application.html.erb. (Note that yield is a standard Ruby keyword for calling a code block (higher-order method) argument.)

So let’s add some code to display the current user state to the layout:

<div class="login-bar">
  <% if user_signed_in? %>
    Welcome, <%= current_user.email %>
    &nbsp;|&nbsp;
    <%= link_to "log out", destroy_user_session_path(user_session), :method => :delete %>  
  <% else %>
    <%= link_to "log in", new_user_session_path %>
    &nbsp;|&nbsp;
    <%= link_to "register", new_user_registration_path %>
  <% end %>
</div>

Next, let’s register for an account. Since by default most machines don’t allow outgoing mail, we can look through log/development.log instead to find the confirmation link. We visit the specified page, which confirms the account, and then we can log in.

Finally, another useful purpose for layouts is providing a standard way to report messages to users. Devise, some other libraries, and possibly your own code will want to do this (e.g., devise provides messages about whether login was successful). Often these messages are stored in the flash hash table, something provided by Rails. So let’s edit app/views/layouts/application.html.erb to also include

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

Now try logging in and out, and you’ll see appropriate messages from devise.

Basic access control

Next, we want to set things up so that only logged-in users can create talks. This is really easy to do with devise. Just edit TalksController to include a filter:

class TalksController < ApplicationController
  before_filter :authenticate_user!
  ...
end

This call tells Rails to call the method authenticate_user! before calling any action in the controller. The filter method must either succeed and return as normal, in which case the action will be called as usual. Or, the filter can redirect the user to another page. In this case, a failure of authenticate_user! will redirect the user to a sign in page.

The above works, but it’s a little too restrictive. We’d actually like the index action to be excluded from this requirement. This is also easy:

before_filter :authenticate_user!, :except => [:index]

Associating talks with users

It seems fine that any user can create a talk, but it seems poor that any user can edit any other user’s talk. As a first step in fixing this, we need to associate users with talks. Each talk should have exactly one user as an owner. Thus, in essence, we need a pointer from talks to users. Rails has special support for these kinds of associations, as well as many others. Full details can be found in A Guide to Active Record Associations.

Recall that every row in a Rails database table has a unique identified. We can use that unique identifier as a pointer:

$ rails g migration AddUserToTalk user_id:integer
# Adds an appropriate foreign key to each talk

To tell Rails that this pointers exists we add

class User < ActiveRecord::Base
  ...
  has_many :talks
end
class Talk < ActiveRecord::Base
  belongs_to :user
end

(Notice the singular/plural different. Rails has a lot of places where this matters, so be careful.) Calling has_many and belongs_to gives those classes methods for access the talks correspond to a user, or the user corresponding to a talk. The following edits will make this clear:

# TalksController#create - add the line
@talk.user = current_user

# app/views/talks/show.html.erb - add text

  <p>
    <b>Owned by:</b>
    <%= @talk.user.email %>
  </p>

# add similar to app/views/talks/index.html.erb, if desired

Notice that the last bit of code assumes that there is a user for a particular talk. In the talks we’ve created so far there hasn’t been one, so in fact @talk.user might return nil right now. So we need to either modify that record (e.g., by editing the database directly); write code to handle that case; or in our migration, add a default initial value. For more on migrations, see Migrations.

Richer access control

Now we want to modify TalksController#update to require something like

current_user == @talk.user

We could put that test right there, but what happens if we want to modify the condition in the future? For example, we might want to allow admin users to edit any talk. Or, if talks are grouped into lists, then perhaps a list owner shoudl be able to edit any talk on a list. Thus, it seems like poor practice to sprinkle the access control policy through our code.

Instead, we’d like to separate policy from mechanism. A nice way to do this is with the CanCan gem.

# Add "gem 'cancan'" to Gemfile
$ bundle install
# Restart rails
$ rails g cancan:ability # makes app/models/ability.rb

CanCan’s Ability model is used to describe an access control policy. Let’s edit +Ability#initialize# to state that users can edit talks they own:

class Ability
  ...
  def initialize(user)
    user ||= User.new
    can :edit, Talk, :user_id => user.id
  end
end

The initialize constructor is called with the current user to determine abilities. Cancan includes support for a standard set of actions, like :edit, but you can add whatever actions you like. Now we can modify TalksController#edit and #update to include

authorize! :edit, @talk

This will check that the user has the right permission, and will raise an exception if not. Notice that we put the authorization check in two places: the controller to GET the view with the form, and the controller to which the form is POSTed. Don’t forget to do both! (Definitely don’t forget to cover the POST (update) controller!)

Give this a try by creating another user and trying to edit a talk created from a different account.

The nice thing about using CanCan is that if we change our policy, we just need to update the Ability class and not the controller code. For example, if we added an admin? flag to users, we might write

class Ability
  ...
  def initialize(user)
    user ||= User.new
    can :edit, Talk, :user_id => user.id
    if user.admin? # would need to add this to User model
      can :edit, :all
    end
  end
end

XSS and CSRF attacks

See slides.

Other possible topics, depending on time