Tuesday, November 26, 2013

Securing Rails, Part III - Direct Access

In this third an final section on securing rails, we get to the heart of the matter and the original reason I started writing this security summary. It seems there are two critical security issues that Rails has that require developer intervention on a case by case basis. Mass Assignment (well known and discussed everywhere) and Primary Key Exposure (hardly a peep from the web on this one).

For the Mass Assignment problem, a raft of articles have been written about this. Here's a great one. Please note that this falls under one of OWASP's top ten list, specifically 2013's A7. The articles previously referenced contain solutions for mass assignment, so I won't repeat it here. If you're using Rails 4.0.0 and Ruby 2.0.0 or later, you should be covered. If not, Michael Hartl (of Rails Tutorial fame) has written a Mass Assignment detector plug-in. Automatic problem detection fits right in with TDD. If you give that a try, leave a comment regarding how you like it.

A note about Mass Assignment and Authorization: it isn't enough to restrict assignment to whitelisted attributes, attribute assignment must happen only within the proper context. For example, a user's password may be changed, so it's password (and password_confirmation) attributes are on the whitelist. However, for security purposes, we may choose to allow a password to be changed only after the user has correctly entered the current password. This state transition must be checked, otherwise an attacker may by-pass the security protocol.

My greatest concern with Rails, however, is its Primary Key Exposure problem. This security vulnerability is so prevalent, it's completely ignored by Rails developers as a feature and not a bug. Some systems swap the Primary Key value for a search engine friendly value in the URI. Often times, these friendly URIs retain the same security problem as they allow for even more easily guessed values. When a user wishes to expose information publicly a friendly URI is a boon. In that case, consider this gem, friendly_id.

In the default case, when the Rails system transforms object references into web pages (and JSON structures), references to objects use the object's Primary Key, the data value used by the underlying database. Why is this a Bad Thing? Primary Keys are almost always created in order and start with 1 (unique, monotonically increasing value). This means that object references are easily guessable. If your authorization model is poor, non-existent or simply has a bug, an attacker has access to users and/or their data. OWASP places this as item A4 on their 2013 top ten. Furthermore, an attacker can compound security holes using exposed Primary Keys. For example, a SQL injection attack becomes easier because the attacker has a rich supply of object IDs or, at least, knows how to form them. If the attacker knows a time interval within which a user has signed up for your service, they can bound the user IDs they need to guess that correspond to that user.

A proper, Defense in Depth strategy, recommends swizzling these Primary Keys into large, random and, therefore, hard to guess values. The sparse range of object IDs makes it more difficult for an attacker to locate a valid ID, let alone locate a particular object's ID. The controller uses the swizzled Primary Key passing it to the view for distribution to a web client or other application. This means changing the Model's to_param() method. It also requires a method to do the reverse - convert back into an object from the external ID. A recommended approach is to create a from_param() method. In addition, you need to add a new attribute to your model, for example, OID.

To generate a 120 bit random value (120 fits in 20 url safe bytes), use the following code:
self.oid = SecureRandom.urlsafe_base64(20*3/4)
Putting it all together, add this code to your <model>.rb file

  before_validation :check_oid

  MinOIDLength = 20
  validates :oid, length: { minimum: MinOIDLength }
  validates_uniqueness_of :oid

  def to_param
    oid
  end

  def self.from_param(param)
    find_by_oid(param)
  end

  def check_oid
    if !self.oid
      self.oid = SecureRandom.urlsafe_base64(MinOIDLength*3/4)
    end
  end



This allows the oid attribute to be changed, when really it should be read-only, however, this code works. If you have a better suggestion for how to implement an automatically added and complex external ID, leave it in the comments.

No comments:

Post a Comment