Securing your Rails models with ModelSecurity.

Bruce Perens
bruce@perens.com
Vice President, Sourcelabs.

Last modified: Wed Oct 5 14:26:14 PDT 2005

ModelSecurity helps Ruby on Rails developers implement a security defense in depth by implementing access control within the data model.

If you are like most developers, you think about security when you program controllers and views. But a bug in your controller or view can compromise the security of your application, unless your data model has also been secured.

The economical, flexible, and extremely readable means of specifying access controls provided by ModelSecurity makes it easier for the developer to think about security, and makes security assumptions that might otherwise live in one developers head concrete and communicable to others.

Defense in depth is a common military strategy to prevent an attack from succeeding. If the attacker gets through the first level of defense, there's another level there to meet him. Defense in depth is useful to programmers because it will reveal a flaw for repair in the first level while simultaneously stopping the attack at the second level. Most developers implement some security features in their controllers and views: for example, a controller will generally not allow access to administrative features unless the user is logged in as an administrator, and your views should not display data that is inappropriate for a particular user. This is the "first layer of defense". At that point, we come to the data model access permissions: your second layer of defense. If you've specified them, ActiveRecord will not allow the data to be revealed or modified in an inappropriate manner. Views will in general quietly fail to present non-permitted material in a way that can be exploited: it will be blank if it's not readable, and will be presented as static data rather than in an edit field if it's not writable. This behavior facilitates scaffolding, which would display and allow modification of everything if not told otherwise. A non-permitted controller access to the data model will result in a SecurityViolation exception being raised, which causes the action to terminate and logs diagnostic information that will assist the developer to locate the controller bug.

ModelSecurity adds these facilities to Rails:

Getting Started

Before we go through the features of ModelSecuity, it's useful to get working code on your system. To go through this demo, you'll need to have Rails, RubyGems, and MySQL installed.

Generate a rails program called test, and change to its directory:

rails test
cd test

Install the model_security gem:

gem install model_security_generator
  

Generate model_security for your application

script/generate model_security -
  

Have MySQL execute the script to create the database and a MySQL user ID for the rails program:

cd db/
mysql -u MYSQL-ADMIN-NAME -p < demo.sql

Examine demo.sql and users.sql . users.sql defines the table of users. An accessory table defined by user_configurations.sql is used to hold configuration choices for the user system. When you include ModelSecurity in another Rails program, you'll skip demo.sql and use users.sql and user_configurations.sql to create their tables.

Edit the database configuration in db/database.yml to look like this:

development:
  adapter: mysql
  database: model_security_demo
  host: localhost
  username: m_s_demo
  password: 

Add some necessary facilities to your brand new application controller:

cd ../app/controllers
cp ADD_TO_APPLICATION_CONTROLLER.ModelSecurity application.rb
cd ../..

If this wasn't a new controller, you would edit application.rb to add the facilities requested, rather than copying over the file.

Start the rails server:

script/server &

Open a web browser to http://localhost:3000/user/new Use the form to create a new user. That user will automatically be granted the administrator role, and will be activated immediately.

Subsequent users will not automatically get the administrator role, although an administrator can grant it to them by editing their record with /user/edit/user-name . Subsequent users will not have their login activated immediately when they create it. Instead, they will be sent an email containing a URL that they must use to activate their login.

Now, you can play with the demo. The user controller responds to:

/user/activate/id?token=security-token
Activate a user, after the user's login has been created.
/user/configure/id
Configure the User system. Currently allows you to choose: The configure function requires the administrator role.
/user/destroy/id
Destroy a user. Requires the administrator role.
/user/edit/id
Edit the attributes of a user. Administrators are allowed to edit more fields than normal users, and normal are allowed to edit their own record, not anyone else's.
/user/forgot_password
Send an email to the user that facilitates password recovery.
/user/list
List the users. Administrators can see more information than normal users. Normal users can see some information on their own record that they will not see in the records of other users.
/user/login
Perform HTTP authentication to log in a user. If that doesn't work, fall back on a login form.
/user/logout
Log a user out. Actually loops back to the login action, because the only way to get the browser to stop sending HTTP authentication data with every request is to ask it to get new authentication data from the user.
/user/new
Create a new user.
/user/show
Display information about a user. Administrators can see more information than normal users. Normal users can see some information on their own record that they will not see in the records of other users.

Now that you have the software running, create a user using /user/new The first user that you create will automatically be granted the administrative privilege, sort of like "super-user" status on a Unix or Linux system, and will be logged in immediately.

You should be able to grant administrative status to subsequent users by editing their record and setting the admin field to 1, due to a Rails bug (at this writing, October 2005), the scaffold views aren't capable of editing boolean database fields. We won't need that facility to go through our demo.

Log out of the application via the /user/logout action. Your browser will present a login panel using HTTP authentication. Use the cancel button or whatever mechanism your browser provides to get rid of that panel. You should now see a login form in an HTML page. Don't log in.

Run the /user/list action. This action requies a login before it will complete. The browser will present the login panel again. Log in to the application as your administrative user. You'll now see the list of users.

Learning About Login Management And The User Object

Let's look at the code that made that login panel happen. In app/controllers/user_controller.rb, you will find this code:

# Require_login will require a login before the action can be completed.
# It uses Modal, so it will continue to the action if the login is
# successful.
before_filter :require_login, :only => [ :list, :show ]
  

That code causes require_login to be called before the list or show actions. Require_login will keep trying to get a user logged in until the login succeeds or the user bails out of the process using the back button. If you don't yet understand how before-filters work, you'll need to peruse the Rails documentation. If a user is already logged in, require_login returns true and execution proceeds. If no user is logged in, require_login will save the current action and then redirect the browser to the /usr/login action. Once the user is logged in successfully, the browser will be redirected to the previous action.

Although you might believe that all of this is taking place sequentially in your Rails application, that's not the case. It's achieved by sending a redirect to the browser as each step completes. At each step the browser requests the URL it's been redirected to, and Rails carries out an individual action. Execution proceeds this way if the user asks for /user/list while not logged in:

  1. The user asks for /user/list
  2. Rails redirects the browser to /user/login
  3. The browser asks for /user/login
  4. Rails sets the HTTP header to request HTTP authentication and sends a login form.
  5. The browser sends login information.
  6. Rails redirects the browser to /user/list
  7. The browser requests /user/list
  8. Rails finally sends the list of users.
Fortunately, all of this complication is handled for you. All you need to know is that you should set require_login as a before-filter for any action that should only be executed for logged-in users.

There's also a require_admin filter. This one is used with the destroy action of the user controller, as shown by this code:

# Require_admin will require an administrative login before the action
# can be called. It uses Modal, so it will continue to the action if the 
# login is successful.
before_filter :require_admin, :only => [ :destroy ]
  

That will prevent the destroy action from being executed unless the user successfully logs in as an administrator.

require_admin and require_login are part of your controller-based security setup. As the first line of defense, they will be where potential security violations should stop.

There's only a little more to learn about logging in and users. Take a look at the file app/controllers/application.rb . You'll see these lines:

require 'user_support'

class ApplicationController < ActionController::Base
helper :ModelSecurity
include UserSupport
before_filter :user_setup
end

Requiring the file user_support and including the module UserSupport mix the login and user-management capabilities into the application controller's class. The before-filter user_setup handles session control and login management for the entire application. Once user_setup has run, the User object for the currently-logged-in user can be accessed through User.current .

Introducing ModelSecurity

That's all you need to know to use the User system to manage logins. But we'll keep looking at the User code to see how it secures its own data using ModelSecurity.

Create a second user with /user/new . This user won't be privileged. The system will send you an email with instructions on how to confirm that user's login. Just in case email delivery isn't working on your server, you can get the email text out of the log file logs/development.log . Confirm the user's login.

Logged in as your administrative user, visit /user/list . That will display a list of the users you've created so far. Because you are the administrator, their emails will be visible to you. Now, log out using /user/logout and log back in as the non-privileged user you created. Visit /user/list again. Now, you'll be able to see your own email, but not that of other users.

Let's look at the code that makes that happen. Edit app/models/user.rb and find these lines:

# If this is a new (never saved) record, or if this record corresponds to
# the currently-logged-in user, allow reading of the email address.
let_read :email, :if => :new_or_me_or_logging_in?
  

This code, in the declaration of the User class, says that the email attribute of the model can be read if the function new_or_me_or_logging_in? returns true. Let's look at that function:

# Return true if the user record is new (never been saved) or if it
# corresponds to the currently-logged-in user, or if the current user
# is the special "login" user. This security test is a common pattern
# applied to a number of user record attributes.
def new_or_me_or_logging_in?
  new_record? or User.current == self or logging_in?
end
  

The first condition is the Rails function new_record? which would return true if the record has never been saved. The second condition would return true if the record refers to the currently-logged-in user, and this is what allows the user to see his own email address. The third condition returns true if there is currently no logged-in user (it contains the expression User.current == nil). These are all of the cases in which the application should allow reading of the email attribute of a User record, except one.

NOTE: at this time, there is a app/views/user/list.rhtml template installed by the generator that overrides the scaffold. The difference between this file and the scaffold template is that ActiveRecord#readable? is used to determine if a datum should be displayed or not. This file will move to app/views/scaffolds/list.rhtml once I figure out how to override the scaffold template location. - Bruce

What tells the software that the administrator can read everyone's email address? That's another line in the User model declaration:

# Let the administrator access all data. This implements a Unix-like
# super-user. Note that the coarse-grained override of the super-user
# is not a _necessary_ pattern for the ModelSecurity module, you can
# implement controls as fine-grained as you like.
let_access :all, :if => :admin?

let_access calls let_read and let_write with the same arguments, and thus sets both read and write permissions. :all is a special name that means the permission applies to all of the attributes of the data model, not just one database field. And admin? is a function that returns true if the currently-logged-in user has the administrative privilege. It just tests the value of a model attribute called admin. The effect of all of this is that the administrative user can read or write any datum.

You must start by setting the permissions for :all in your model if you are using ModelSecurity, as the default is to permit all accesses. If you won't be setting up an administrative user, as above, it would be a good idea to make the default "no access". You can do that with the following code:

# Set the default to "no access".
let_access :all, :if => :never?

The never? test is provided with ModelSecurity, and always returns false. A companion test always? reliably returns true.

ModelSecurity has two mechanisms for protecting data. One protects the actual data model and will throw a exception upon an unpermitted access. But that's not what we've been seeing: our views have quietly refused to display some unpermitted data, without any exceptions. A second mechanism prevents the common view functions, including those used by templates, from displaying or editing data inappropriately. You activate that mechanism by declaring ModelSecurityHelper in your application, as below in app/controllers/application.rb:

require 'user_support'

class ApplicationController < ActionController::Base
helper :ModelSecurity
include UserSupport

before_filter :user_setup
end

This makes the code in ModelSecurityHelper available to all views in the application. ModelSecurityHelper redefines these common view functions to respect the data model permissions:

Views that use those functions exclusively to display and edit database fields will always refrain from making inappropriate read or write accesses.

One complication of the view protection is that instance variables of your model that are not related to database fields are effected in the same way as model attributes. So, when you declare the accessors for those instance variables with code like this:

attr_accessible :password, :password_confirmation, :old_password

You should also declare security permissions on those same variables:

# NOTE: :password, :password_confirmation, and :old_password
# are not attributes of the record, they are instance variables of the
# class and aren't written to disk under those names. But I declare them
# here because otherwise ModelSecurityHelper (which doesn't know that)
# isn't going to allow me to enter them into a form field.
#
# I like how fine-grained I can get.
let_write :password, :if => :new_or_me_or_logging_in?
let_write :password_confirmation, :if => :new_or_me?
let_write :old_password, :if => :me?

Display Control

You may also have noticed that there are more fields in the database, and more attributes in the User object, than the view displays when you visit /user/list . But a quick perusal of the code will show that it's a scaffold view. You would have expected a scaffold to display all fields. But ModelSecurity allows us to override that with the let_display declaration. Take a look at its use in the User model declaration:

# These just control display.
let_display :all, :if => :never? # Set default to don't display, override below.
let_display :admin, :activated, :if => :admin?
let_display :login, :name, :email

This declaration will cause the scaffold view to present information about three fields, login, name and email to all users (although a let_read declaration further restricts whose email address a non-privileged user can see, as above). Administrative users will also see the fields admin and activated. Nobody will see the fields cypher and salt, they are long strings of encrypted or random cybercrud that nobody wants to read. Note that the scaffold view will format the display as a table with columns only for the data that it will display. All of this is achieved by overloading the content_columns method of ActiveRecord to filter the columns it reports using the data in the let_display declaration.

While the tests used with let_read, let_write, and let_access are instance methods of your ActiveRecord class, tests used with let_display must be class methods. This is because the decision of what columns to display is made before the first row's instance is accessed. Rails makes it easy enough to declare a method to work with both an instance and its class.

You've now completed the ModelSecurity tutorial. For more information, see the reference.