Section 8.1. Healthy Skepticism: Don t Trust User Input

8.1. Healthy Skepticism: Don't Trust User Input

The golden rule of web application security, the mantra you should be chanting in your sleep, is don't trust user input. Or in Cold War terms: trust, but verify. Most security vulnerabilities boil down to this one principle, but it's not always obvious on the face of it. All user input is susceptible to modification. Regular form fields are the most obvious means of user input, but there are far more: hidden form fields, cookies, URL parameters, POST data, HTTP headers, and Ajax requests. It's all user input, all modifiable, and all shouldn't be trusted.

In this section, we'll examine the most important practical examples of this dictum.

8.1.1. Using Scoped Queries

Ironically, one of the most obvious pieces of information that a user can fake is also one of the most overlooked: record IDs. In most Rails applications, database record IDs (usually sequential numbers) are used right in the URL, just begging for curious users to fiddle with them. When Ajax is involved, the URLs might not be visible in the address bar, but they're just as vulnerable to change.

Suppose you've founded a startup to develop an Ajaxified, Web 2.0 address book application. To start out, you've just got two models, User and Contact:

class User < ActiveRecord::Base   has_many :contacts end class Contact < ActiveRecord::Base   belongs_to :user end

Simple enough. Shifting attention to your controllers, you rough in this implementation for the first few actions in the contacts controller:

class ContactsController < ApplicationController   before_filter :require_signin   def new     @contact =   end      def create     contact = params[:contact]     contact.user_id = session[:user_id]     redirect_to contact_url(contact)   end      def show     @contact = Contact.find params[:id]   end   private     def require_signin       return false unless session[:user_id]     end end

Then you create some quick views, and try it out. Everything works perfectly, so you move on to the next problem...without realizing there's a big security hole in the code. Take a closer look at the show action. It would be accessed with a URL path, for example /contacts/42, making the value of params[:id] be 42. The action looks up the corresponding Contact record and displays it. Sure, the before_filter ensures that the user is signed in, but there is nothing to make sure that contact #42 belongs to the current user. When users start poking around (and they will), they'll have full access to every other user's little black bookwhich is sure to put a damper on your launch party.

The solution is to appropriately scope your queries. In this case, that means that contacts should only be selected within the scope of the current user. Here's a safer and more robust implementation:

class ContactsController < ApplicationController      # gives us a @current_user object   before_filter :require_signin   # safely looks up the contact   before_filter :find_contact, :except => [ :index, :new, :create ]      def index     @contacts = @current_user.contacts.find :all   end      def new     @contact =   end      def create     @current_user.contacts.create params[:contact]     redirect_to contacts_url   end      def show   end      def edit   end      def update     @contact.update_attributes params[:contact]     redirect_to contact_url   end      def destroy     @contact.destroy     redirect_to contacts_url   end   private     def require_signin       @current_user = User.find session[:user_id]       redirect_to(home_url) and return false unless @current_user     end     def find_contact       @contact = @current_user.contacts.find params[:id]     end end

Now the Contact model is never directly accessed at all. Instead, it's all scoped using the contacts association on the @current_user object. That way, there's no way that one user can seeor worse, changeany other user's data.

8.1.2. Record IDs in URLs

The scoped queries example illustrates how record IDs in URLs (e.g., /contacts/42) should be verified before being used. But in some cases that's not possible, and merely having a guessable identifier in the URL opens the possibility of abuse.

For example, suppose you want to offer personalized, private RSS feeds to your users. Many feed readers don't support any kind of authentication, so the feeds need to be publicly accessible. But if the only thing differentiating each feed URL is a sequential number (e.g., /feeds/123), they're easily discoverable.

In many cases, an acceptable compromise is to use random strings as identifying tokens, instead of sequential record IDs. For example, you might add a column called token to the users table and then create a randomized string every time a new User model is created, like this:

class User < ActiveRecord::Base      def before_create     token = Digest::SHA1.hexdigest("#{id}#{rand.to_s}")[0..15]     write_attribute 'token', token   end end

Then, on the controller side, you simply look up the user according to their token, rather than their ID:

class FeedsController < ApplicationController      def show     @user = User.find_by_token(params[:id]) or        raise ActiveRecord::RecordNotFound   end end

With that, the feed URLs are practically un-guessable (/feeds/34fc89fe735a7837) and still convenient with clients that don't support HTTP authentication.

8.1.3. Mass Assignment

Of course, record IDs aren't the only kind of user data that shouldn't be trusted. Rails provides other conveniences that make it easy to create an insecure application. One such convenience is known as mass assignment, or updating multiple record attributes with one command. For example, the create and update actions in the ContactsController class use mass assignment:

contact = current_user.contacts.create params[:contact] contact.update_attributes params[:contact]

ActiveRecord objects can be passed a hash (in this case, params) where the hash keys correspond to the record's attributes and all the attributes are set at once. That's great much of time, but what if some attributes shouldn't be editable?

For example, consider our address book application. As development progresses, you might want the user to have a profile page, where they can edit their account settings. Easy enough. You create a controller with two actions, like this:

class UsersController < ApplicationController   def edit     @user = current_user   end      def update     current_user.update_attributes params[:user]     redirect_to edit_user_url   end end

Then you create a view for the edit action, edit.rhtml:

<% form_for :user, :url => user_url, :html => { :method => :put } do |u| %>   <p>Login: <%= u.text_field :login %></p>   <p>Password: <%= u.password_field :password %></p>   <p><%= submit_tag "Save Account Settings" %> <% end %>

Notice that the update action uses mass assignment to update the user record. So submitting the form will create a params hash like { :user => { :login => "scott", :password => "secret" } }, which in turn causes both the login and password attributes of current_user to be updated.

So far, there's no problem. But suppose you later add a new attribute to the user model, for example is_administrator, so that you can differentiate between regular users and admins. Without some caution, adding that attribute will seriously expose your application to a security attack. Form submissions are trivially easy to fakesuch as with a three-line Ruby program like this:

require 'net/http' http = 'localhost', 3000 "/users/1", 'user[is_administrator]=1&_method=put',   { 'Content-Type' => 'application/x-www-form-urlencoded' }

Because update_attributes (as well as other mass assignment methods, new, create, and attributes=) simply overwrites any attribute with the same names as the params keys, anyone could grant themselves administrator privileges to the application.

The solution is ActiveRecord's attr_protected. It's a class-level method that allows you to declare certain attributes to be immune to mass assignment. For example, here's a modified User model:

class User < ActiveRecord::Base   attr_protected :is_administrator   has_many :contacts end

With that attribute marked as protected, any attempt to change it via mass assignment will be ignored. For example, look at the result of using update_attributes to set the protected attribute:

>> scott = User.find 1 => #<User:0x3291f8c ... > >> scott.is_administrator? => false >> scott.update_attributes :is_administrator => true => true >> scott.is_administrator? => false

As you can see, update_attributes doesn't have any effect on the protected attribute. To actually change it, you would use the specific setter, for example:

def update   current_user.update_attributes params[:user]   current_user.is_administrator = params[:user][:is_administrator]   redirect_to edit_user_url end

Every time you add a new attribute to an ActiveRecord model, stop to think about who should have permission to modify it and which controllers might interact with it. When in doubt, consider protecting it with attr_protected.

In some cases, you'll want to err on the side of caution and disallow mass assignment by default. Rails provides for that approach as well, with attr_accessible. It works just like attr_protected, but in reverse: by default, every attribute will be protected, except those marked as accessible. For example:

class User < ActiveRecord::Base   attr_accessible :login, :password   has_many :contacts end

Now, you can freely add columns to the users database table, knowing that they won't be writable via the mass-assignment methods unless you specifically provide for them to be.

8.1.4. Form Validation

Before Ajax, the prototypical use of JavaScript was client-side form validation: for example, using JavaScript to stop the form from submitting if a required field is left blank, a phone number was improperly formatted, etc. Because validation happens completely on the client side, without requiring a round-trip to the server, the feedback can be immediate.

But it's critical to remember that client-side form validation is a convenience and is not sufficient by itself. Even if the client-side validation passes, the data should still be validated on the server side as well.

8.1.5. SQL Injection

SQL injection is a security breach that can happen if you pass input directly from the user to the database. Of course, most applications would never intentionally allow a user to input a full query to the databasewith one command, anyone could wipe out all your data.

But it's surprisingly easy to create such a gaping hole in your application. The trick is that malicious users can hijack your queries to send custom SQL to the databasepotentially revealing, altering, or even deleting data.

For example, consider this ActiveRecord statement:

# unsafe User.find(:first, :conditions => "login    = '#{params[:login]}' AND                                   password = '#{params[:password]}'")

The :conditions option for find essentially defines an SQL WHERE statement. In this case, we're taking two parameters from a form submission and interpolating them directly into a string. So when a good user signs in, the resulting SQL will look like this:

SELECT * FROM users WHERE (login='alice' and password='secret') LIMIT 1

But now suppose a malicious user attempts SQL injection. Instead of entering a password in the form, they enter an SQL snippet, like:

' or login='bob' and password != '

Now, the resulting SQL looks like this:

SELECT * FROM users WHERE (login='' and password='' or login='bob' and password !='') LIMIT 1

As a result, the attacker is able to log in as any other user of the systemwithout providing the password. And that's not even the worst of the potential consequences. Depending on the database used, it may even be possible for attackers to execute arbitrary statements, such as DELETE from users WHERE 1=1.

The rule is simple: never include tainted data (i.e., anything that could have potentially come from user input) directly in an SQL statement, including clauses such as the :conditions option. Instead, allow Rails to escape the data by passing a hash to :conditions, like this:

# safe User.find(:first, :conditions => { :login    => params[:login],                                    :password => params[:password] })

Each of the elements of the hash will be joined by AND, and the key/value will be compared for equality. If you need more flexibility (clauses joined by OR, comparisons such as less-than), use this form:

# safe User.find(:first, :conditions => [ "login    = :login AND                                     password = :password",                                  { :login    => params[:login],                                    :password => params[:password] } ])

The above code demonstrates using named keys, but it's also possible to use this shorter form:

# safe User.find(:first, :conditions =>    [ "login = ? AND password = ?", params[:login], params[:password] ])

In all three of the last examples, the user-provided data will be properly escaped before being inserted into the SQL statement, protecting you from SQL injection attacks.

8.1.6. Session Fixation

Session fixation is a type of security attack on web applications that intentionally sets a user's session key to a known value. There are several ways this can be done, but the most common works like this: an attacker requests a page from your application, and Rails returns a session ID in the Set-Cookie response header. Then, the attacker gets a legitimate user to send the same session ID with their next request to the application. Rails makes this step difficult by only recognizing session IDs from cookies, as opposed to GET or POST parameters. However, some browsers have buggy cookie implementations, allowing one site to plant cookies on a browser that will be delivered to another sitea class of attack referred to as cross site cooking. The user is prompted to sign on, and once they do, the attacker effectively has the key to that user's account.

To thwart this potential security breach, it's a good idea to generate a new session ID when a user authenticatesthat way, the attacker will just be left with an expired session.

Here's how a standard sign-in and sign-out action might be implemented, using reset_session to generate a new session ID after authenticating.

# presumes a route like: map.resource :session class SessionsController < ApplicationController      skip_before_filter :require_signin   # signin   def create     if u = User.find_by_login_and_password(params[:login],                                            params[:password])       reset_session # create a new sess id, to thwart fixation       session[:user_id] =       redirect_to home_url     else       render :action => 'new'     end   end      # signout   def destroy     reset_session     redirect_to new_session_url   end end

The essential element to avoiding a session fixation attack is the second line in the create action, reset_session. That will wipe out the current session, including its ID, and create a new, blank one. The authenticated user's ID is then stored in the new session, and an attacker won't have the new, randomly generated session ID.

8.1.7. Cross-Site Scripting

Cross-Site Scripting (often abbreviated XSS, to avoid confusion with CSS) is another type of attack on web application securityand yet another example of the principle don't trust user input. In the case of SQL injection, problems surfaced when unescaped user data was included in SQL queries. In the case of XSS, vulnerabilities emerge when unescaped user data is included in HTML output.

It's a little less obvious how this is a problem. Obviously, handing over control of the database is bad, but what harm can come from plain HTML? The answer is JavaScript. Because executable JavaScript can be inserted into HTML, it's not just a passive data formatin effect, HTML becomes running code.

For example, consider adding a search engine to your intranet application. First you'd create a simple form to accept the query:

<%= start_form_tag search_url, :method => :get %>   <p><%= text_field_tag :q %> <%= submit_tag "Search" %> <% end %>

The action behind search_url might then be implemented like this:

class SearchController < ApplicationController      def index     @q = params[:q]     @posts = Post.find :all,                :conditions => ["body like :query",                                { :query => params[:q]}]   end end

And finally, the view displays the results:

<p>Your search for <em><%= @q %></em>   returned <%= pluralize @posts.size, "result" %>:</p> <% @posts.each do |post| %>     <li><%= link_to post.title, post_url(:id => post) %>:       <%= exerpt post.body, @q %></li> <% end %>

Can you spot the security hole? The problem is that user inputnotably the search query stringis being directly passed to the page output. That means an attacker can feed arbitrary data, such as JavaScript, into the page. Consider a URL like this, with a JavaScript command in URL-encoded form:'XSS')%3B%3C%2Fscript%3E

If an attacker is able to trick a user of the system to follow that URL (perhaps by including it in an email), then he's able to execute arbitrary JavaScript from the context of a logged-in user. In this example, the attack payload is merely a JavaScript alert. But the injected script could just as easily use Ajax to modify the intranet, or even silently send private information (like the user's session key) back to the attacker. The private system is effectively wide open.

The solution is simple: the h helper, also known as html_escape. This helper (actually provided by the ERb templating system, not Rails itself ) escapes HTML strings by making four simple substitutions: it converts &, ", >, and < into &amp;, &quot;, &gt;, and &lt;, respectively. The result is that any attempt to inject <script> tags (or for that matter, any HTML) is neutered.

Use it like any other helper:

<p>Your search for <em><%= h @q %></em> <%= link_to h(, user_url(@user) %>

It's a good idea to train your fingers to automatically reach for the H key when you are writing ERb tags, because it will eliminate a large class of XSS vulnerabilities.

Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

Similar book on Amazon © 2008-2017.
If you may any questions please contact us: