One of the easiest means of cracking a naively-implemented Rails application (or any RDBMS-backed application with auto-incremented primary keys) is to simply alter site URLs and request parameters with the assumption that their IDs will be sequential.
For example, if a user sees a URL like the following in their location bar:
http://example.com/post/2705/comments
They could easily replace the number 2705 with, say, 2706, and see comments for that post. Of course, you’ve already added explicit permissions checks to every controller method that insure such access will never happen, but we all know how easy it can be to forget to audit each and every method for ID-traversal attempts.
Furthermore, using simple sequential IDs exposes other potentially-useful information to would-be attackers. They can see, for example, how many users, data objects, etc., are in active use on your site, and perhaps even reverse-engineer more of your database schema in order to plan SQL-injection or XSS attacks.
Actually using randomized keys does mean overriding the default ActiveRecord behavior, and may be tough to accomplish simply using ActiveRecord migrations. If your database is of a reasonable size, though, or you’re just getting started, it’s easy enough to do. First, you’ll need to decide on a random ID-generation algorithm.
The best way to generate “random” keys isn’t actually random at all. If you want to insure that no one can guess your keys, you’ll want to use a full-strength cryptographic hash. (See my FOSCON slides from last year for more info on that topic.)
If your primary keys are going to be exposed in your URLs at all, though, there are advantages to keeping them simple. Strong hashes generate long, difficult-to-type identifiers, so you’ll want to either slice them to get only a substring, or use a weaker algorithm which generates less randomness.
Regardless, you’ll need a helper method to generate the IDs, as well as some support in your models and migrations to glue it all together. Here’s a simple example, based on an “account invitation” implementation from an application I’m working on now:
# app/models/invite.rb
class Invite < ActiveRecord::Base
include GenIdHelper
def before_create
self.id = gen_id
end
end
# db/migrate/001_create_invites.rb
class CreateInvites < ActiveRecord::Migration
def self.up
create_table :invites do |t|
t.column :id, :string, :limit => 8
t.column :email, :string
# ...
t.timestamps
end
end
def self.down
drop_table :invites
end
end
# lib/gen-id.rb
module GenIdHelper
# Generates a pseudo-random string in hex format (0..9+A..F)
# which contains chunk*16 bits of randomness.
def gen_id(chunks=2)
("%04x"*chunks % ([nil]*chunks).map { rand(2**16) }).upcase
end
end
The end result will be that newly-created instances of the Invite class will have an ID generated by gen_id, and will be much harder for anyone to guess by simply replacing URL components or POST values.
These random IDs can also be considered another form of identification for users. Basically, if they have received the random ID by some trusted channel, (say, SMS message, or snail-mail) it can be treated as a secondary authentication mechanism when creating a new account, performing a password reset, or proving their real world identity.