$ man jerome_

...

rails security pitfalls

| Comments

in this post i will cover the security related stuff for rails which i learned over the last couple of years.

security is hard. rails is pretty secure by default (from hackers? maybe not), but a simple mistake will lead to a catastrophe. as much as possible, try to avoid the common pitfalls. as the saying goes prevention is better than cure

Common Attacks

SQL Injection

Exploits of a Mom

simple case

building your own conditions is vulnerable to sql injection. the code below is not safe.

1
2
3
4
5
6
7
8
User.where("username LIKE '%#{params[:username]}%'")

# query
SELECT
  `users`.*
FROM
  `users`
WHERE (username LIKE '%jerome%')

let say the attacker sets the value of params[:username] e.g.

1
2
3
4
5
6
7
8
9
10
params[:username] = "') UNION SELECT username, password,1,1,1 FROM users --"

User.where("username LIKE '%#{params[:username]}%'")

# query
SELECT
  `users`.*
FROM
  `users`
WHERE (username LIKE '%') UNION SELECT username, password,1,1,1 FROM users --%')

anything after -- will become a comment.

countermeasure

instead of passing the string directly to the condition option, you can pass an array to sanitize the string.

1
User.where("username LIKE ?", "%#{params[:username]}%")

XSS

the rule of the thumb is never trust any data that comes from a user

simple case

let say we have this code:

1
2
3
<span>
  <%= raw user.biography %>
</span>

and the attacker sets the value of user.biography to <script>alert('hello');</script> the rendered page will have this.

1
2
3
<span>
  <script>alert('hello');</script>
</span>

our page will now execute this javascript code!

countermeasure

user input must be sanitized, you can use various Rails methods such as sanitize

1
2
3
<span>
  <%= sanitize (user.biography, tags: %w(a), attributes: %w(href)) %>
</span>

CSRF – Cross-Site Request Forgery

simple case

  • attacker sends requests on victim’s behalf
  • doesn’t depend on XSS

countermeasure

  • use GET and POST methods appropriately
  • use Rails default CSRF protection

Rails Specific Attacks

Mass Assignment

problem

1
2
3
def create
  @user = User.new(params[:user])
end

if you have User.admin attribute; and the attacker sends params[:user][:admin] = 1, then the admin attribute will be set.

solution

  • blacklist attributes using attr_protected
1
2
3
4
class User < ActiveRecord::Base
  attr_protected :admin
  ...
end
  • whitelist attributes using attr_accessible
1
2
3
4
class User < ActiveRecord::Base
  attr_accessible :username, :email
  ...
end
  • use strong parameters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...

def create
  @user = User.new(params_user)
end

private

def params_user
  params.require(:user).permit(
    :username,
    :email)
end

...

Secret Token

this token is used to sign cookies that the application sets. you can generate a token by running $ rake secret

problem

by default the token is stored in the file. most of the time this file is in version control

config/initializers/secret_token.rb
1
MyApp::Application.config.secret_token = 'c38d07e4b543baeea5ae70f5dd670828ec67eb3c4f585020d02667804cb39a3142704028055540b2270c5a8253cea780b16fa353b86f94c17c923ac484a20856'

solution

keep it out of version control. store it in ENV. you can use figaro gem to store data in ENV

config/initializers/secret_token.rb
1
MyApp::Application.config.secret_token = ENV['SECRET_TOKEN']

note: in Rails 4, it is called secret_key_base.

Logging Parameters

by default Rails filter any parameter that matches /password/ from being logged and replacing it with [FILTERED]. you should filter sensitive data from logs.

config/initializers/filter_parameter_logging.rb
1
Rails.application.config.filter_parameters += [:password, :ssn, :token]

“match” in Routing

problem

in Rails 3, there’s even an example in config/routes.rb

config/routes.rb
1
2
3
4
# Example in config/routes.rb
match ':controller(/:action(/:id))(.:format)'

match '/posts/delete/:id', :to => "posts#destroy" :as => "delete_post"
  • match matches all HTTP verb and Rails CSRF protection doesn’t apply to GET requests.
  • the second route will allow GET method to delete posts

solution

  • use the correct HTTP verb e.g: :get, :post, :delete
  • use :via e.g. match '/posts/delete/:id', :to => "posts#destroy" :as => "delete_post", :via => :delete

Scopes

  • let say we have user and post models, when you you retrieve post for edit, update or destroy, make sure you get it through authorize user. in the code below; Example 2 is the preferred way
app/controllers/posts_controller.rb
1
2
3
4
5
6
7
8
9
10
11
...
# Example 1 (UNSAFE)
def edit
  @post = Post.find_by id: params[:id]
end

# Example 2 (SAFE)
def edit
  @post = current_user.posts.find_by id: params[:id]
end
...
  • use authorization gem such as cancan

Admin

most of the time, admin URLs can be found in: http://example.com/admin, if you can, please consider the following:

  • maybe use sub-domain e.g. http://some-url.example.com
  • whitelist IP address
  • VPN or intranet access only
  • separate application

Conclusion

use brakeman to scan your app for vulnerabilities.

1
$ brakeman . -o report.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+----------------------+-------+
| Warning Type         | Total |
+----------------------+-------+
| Cross Site Scripting | 1     |
| SQL Injection        | 1     |
| Session Setting      | 1     |
+----------------------+-------+


+SECURITY WARNINGS+

+------------+-----------------+----------+-----------------+-------------------------------------------------------------------------+
| Confidence | Class           | Method   | Warning Type    | Message                                                                 |
+------------+-----------------+----------+-----------------+-------------------------------------------------------------------------+
| High       | PostsController | set_post | SQL Injection   | Possible SQL injection near line 68: Post.where("id =#{+params[:id]+}") |
| High       |                 |          | Session Setting | Session secret should not be included in version control near line 12   |
+------------+-----------------+----------+-----------------+-------------------------------------------------------------------------+

View Warnings:

+------------+-------------------------------------+----------------------+-----------------------------------------------------------------------+
| Confidence | Template                            | Warning Type | Message                                                                       |
+------------+-------------------------------------+----------------------+-----------------------------------------------------------------------+
| High       | posts/show (PostsController#create) | Cross Site Scripting | Unescaped model attribute near line 10: Post.new(post_params).content |
+------------+-------------------------------------+----------------------+-----------------------------------------------------------------------+

mysql user minimum required privileges for rails

| Comments

if your application uses root as your database username; you’re doing it wrong. a good practice is you should have two users for your application. let say i have a database called blog_production, i will have two users in my database: blog and blog_admin. the former will be use for regular stuff, and the latter for database administration. each db user have diffrent set of privileges.

  • blog user privileges
    • Select
    • Insert
    • Update
    • Delete
    • Lock
  • blog_admin user privileges
    • Select
    • Insert
    • Update
    • Delete
    • Lock
    • Create
    • Drop
    • Index
    • Alter
1
2
3
4
5
6
7
8
9
CREATE DATABASE blog_production;

CREATE USER 'blog'@'localhost' IDENTIFIED BY 'db_password';
GRANT Select,Insert,Update,Delete,Lock Tables ON blog_production.* TO 'blog'@'localhost';

CREATE USER 'blog_admin'@'localhost' IDENTIFIED BY 'db_password';
GRANT Select,Insert,Update,Delete,Create,Drop,Index,Alter,Lock Tables ON blog_production.* TO 'blog_admin'@'localhost';

FLUSH PRIVILEGES;

now, how do you use this two users in your rails app? in databases.yml, add a check for DB_ADMIN env var.

config/databases.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%
  if ENV['DB_ADMIN']
    username = "blog_admin"
    password = "11111111"
  else
    username = "blog"
    password = "22222222"
  end
%>
production:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: blog_production
  pool: 5
  username: <%= username %>
  password: <%= password %>

by default it uses blog db username unless you pass DB_ADMIN=true env var. the reasone we have this env is because we need to pass it when we run migration tasks e.g:

1
$ DB_ADMIN=true bundle exec rails db:migrate

if you use capistrano for deployment, you need to set migrate_env var.

config/deploy.rb
1
set :migrate_env, "DB_ADMIN=true"

if you have better method, hit the comments.

and that’s all there is to it. hth.

rails 4: paypal express checkout integration using activemerchant

| Comments

in this post, i will share on how i integrated Paypal’s Express checkout in a rails 4 application using activemerchant gem. some of the code are based on Railscasts Ep. 146.

before proceeding, make sure you have or did the following:

  • Paypal developer account
  • create Paypal sandbox accounts: merchant and buyer(go to Applications ยป Sandbox Accounts)

Gems to use

1
2
activemerchant (1.42.4)
rails (4.0.2)

Setup Activemerchant

config/environments/developer.rb
1
2
3
4
5
6
7
8
9
config.after_initialize do
  ActiveMerchant::Billing::Base.mode = :test
  paypal_options = {
    login: "API_USERNAME_HERE",
    password: "API_PASSWORD_HERE",
    signature: "API_SIGNATURE_HERE"
  }
  ::EXPRESS_GATEWAY = ActiveMerchant::Billing::PaypalExpressGateway.new(paypal_options)
end
config/environments/test.rb
1
2
3
4
config.after_initialize do
  ActiveMerchant::Billing::Base.mode = :test
  ::EXPRESS_GATEWAY = ActiveMerchant::Billing::BogusGateway.new
end

Model changes

this really depends on your app. maybe you have a carts and orders tables. or bookings and reservations tables. assuming you have carts and orders tables, you’ll have the following fields.

1
2
3
4
5
6
7
8
9
10
11
12
13
carts
 - id
 - total_amount_cents
 - purchased_at
 - created_at
 - updated_at
 
orders
 - id
 - cart_id
 - ip
 - express_token
 - express_payer_id

View Changes

after you add the express_checkout to your routes file; in your cart page, put the express paypal checkout button

app/views/carts/index.html.erb
1
link_to(image_tag("https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif"), express_checkout_path)

IMPORTANT: Paypal wants you to use their buttons, so use them! LOL

Controller Changes

add the following actions in your orders controller.

  • express_checkout – this will setup your purchase and redirect to paypal.
  • new – a.k.a. Paypal’s return URL. i usually add a “Confirm Order” button in this action. it’s a good practice not to process the order right away.
  • create – create and purchase the order
app/controllers/orders_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def express_checkout
  response = EXPRESS_GATEWAY.setup_purchase(YOUR_TOTAL_AMOUNT_IN_CENTS,
    ip: request.remote_ip,
    return_url: YOUR_RETURN_URL_,
    cancel_return_url: YOUR_CANCEL_RETURL_URL,
    currency: "USD",
    allow_guest_checkout: true,
    items: [{name: "Order", description: "Order description", quantity: "1", amount: AMOUNT_IN_CENTS}]
  )
  redirect_to EXPRESS_GATEWAY.redirect_url_for(response.token)
end

def new
  @order = Order.new(:express_token => params[:token])
end

def create
  @order = @cart.build_order(order_params)
  @order.ip = request.remote_ip

  if @order.save
    if @order.purchase # this is where we purchase the order. refer to the model method below
      redirect_to order_url(@order)
    else
      render :action => "failure"
    end
  else
    render :action => 'new'
  end
end

IMPORTANT: the total_amount must be equal to the total amount of items in the array of hashes or else you will get errors.

Model Changes

app/models/order.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Order < ActiveRecord::Base
  belongs_to :cart

  def purchase
    response = EXPRESS_GATEWAY.purchase(order.total_amount_cents, express_purchase_options)
    cart.update_attribute(:purchased_at, Time.now) if response.success?
    response.success?
  end

  def express_token=(token)
    self[:express_token] = token
    if new_record? && !token.blank?
      # you can dump details var if you need more info from buyer
      details = EXPRESS_GATEWAY.details_for(token)
      self.express_payer_id = details.payer_id
    end
  end

  private

  def express_purchase_options
    {
      :ip => ip,
      :token => express_token,
      :payer_id => express_payer_id
    }
  end
end

and that’s all there is to it. hth.

rails 4: login with devise and facebook

| Comments

i recently created an application where you can login with its own authentication system and third party sites like facebook, twitter, linkedin, etc. for this task, i used devise, omniauth and omniauth-facebook gems.

make sure that you setup your facebook app. we’ll use the fb app id and secret later. let’s get started.

Gems to use

1
2
3
devise (3.2.2)
rails (4.0.2)
omniauth-facebook (1.5.1)

Model changes

Create Authentication model

assuming you are using User model for devise, create the Authentication model.

1
2
$ rails g model Authentication user:references provider:string uid:string
token:string token_secret:string

Setup table relations

app/model/users.rb
1
has_many :authentications, dependent: :delete_all
app/model/authentications.rb
1
belongs_to :user

Add methods to user.rb

we need to override devise methods password_required and update_with_password since 3rd party authentications don’t require password.

app/model/users.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  #...
  def apply_omniauth(omniauth)
    self.username = omniauth['info']['nickname'] if username.blank?
    self.email = omniauth['info']['email'] if email.blank?

    authentications.build(:provider => omniauth['provider'],
                        :uid => omniauth['uid'],
                        :token => omniauth['credentials'].token,
                        :token_secret => omniauth['credentials'].secret)
  end

  def password_required?
    (authentications.empty? || !password.blank?) && super
  end

  def update_with_password(params, *options)
    if encrypted_password.blank?
      update_attributes(params, *options)
    else
      super
    end
  end
  #...

Enable omniauth support in devise

add FB app id and secret. note that it’s a good practice to get them from ENV, i use figaro gem to do this.

config/initializers/devise.rb
1
2
config.omniauth :facebook, Figaro.env.fb_app_id, Figaro.env.fb_app_secret,
:scope => 'email, user_location, publish_actions'
app/models/user.rb
1
2
3
4
# add :omniauthable
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable,
       :omniauthable

Controller changes

Create authentications and registrations controller

we need callbacks for omniauth that’s why we’re creating authentications controller and registrations controller to override some of devise functionalities.

config/routes.rb
1
2
devise_for :users, :path => '/', controllers: {omniauth_callbacks:
"authentications", registrations: "registrations"}
app/controllers/registrations_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RegistrationsController < Devise::RegistrationsController

  def create
    super
    session['devise.omniauth'] = nil unless @user.new_record?
  end

  def build_resource(*args)
    super
    if session['devise.omniauth']
      @user.apply_omniauth(session['devise.omniauth'])
      @user.valid?
    end
  end
end
app/controllers/authentications_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class AuthenticationsController <  Devise::OmniauthCallbacksController

  def all
    omniauth = request.env["omniauth.auth"]
    authentication = Authentication.where(provider: omniauth['provider'], uid: omniauth['uid']).take

    if authentication
      flash[:notice] = "Logged in Successfully"
      sign_in_and_redirect User.find(authentication.user_id)
    elsif current_user
      token = omniauth['credentials'].token
      token_secret = omniauth['credentials'].has_key?('secret') ?  omniauth['credentials'].secret : nil

      current_user.authentications.create!(:provider => omniauth['provider'], :uid => omniauth['uid'], :token => token, :token_secret => token_secret)

      flash[:notice] = "Authentication successful."
      sign_in_and_redirect current_user
    else
      user = User.new
      user.apply_omniauth(omniauth)

      session['devise.omniauth'] = omniauth.except('extra')
      redirect_to new_user_registration_path
    end
  end

  alias_method :facebook, :all

end

Views changes

app/views/layout/application.htm.erb
1
<%= link_to 'Login with FB', user_omniauth_authorize_path(:facebook) %>

hide passwords field in devise new and edit templates of registrations if they are not needed.

app/views/devise/registrations/new.html.erb
1
2
3
4
<% if f.object.password_required? %>
  <%= f.input :password, required: true, label: false, input_html: { class: 'form-control' }, placeholder: 'Password'  %>
  <%= f.input :password_confirmation, required: true, label: false, input_html: { class: 'form-control' }, placeholder: 'Confirm Password'  %>
<% end %>

if you want to add twitter, linkedin, etc, just refer to their respective omniauth gems.

and that’s all there is to it. hth.

how i start my controller spec

| Comments

first, setup the data e.g.

1
2
let(:user) { FactoryGirl.create(:user) }
let(:admin) { FactoryGirl.create(:admin) }

then define who has access to it e.g.

1
2
3
4
5
6
7
8
describe "admin access" do
end

describe "user access" do
end

describe "guest access" do
end

if needed, i use shared examples e.g.

1
2
3
4
5
shared_examples("public access to products") do
end

shared_examples("full access to products") do
end

and call them like this:

1
it_behaves_like "public access to products"

here’s the full source for reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
require 'spec_helper'

describe ProductsController do

  let(:user) { FactoryGirl.create(:user) }
  let(:admin) { FactoryGirl.create(:admin) }

  shared_examples("public access to products") do
    describe "GET #index" do
      before :each do
        get :index
      end

      it "renders the :index template"
    end
  end

  shared_examples("full access to products") do
    describe "GET 'new'" do
      before :each do
        get :new
      end

      it "renders the :new template"
    end
  end

  describe "admin access" do
    before :each do
      sign_in :user, admin
    end

    it_behaves_like "public access to products"
    it_behaves_like "full access to products"
  end

  describe "user access" do
    before :each do
      sign_in :user, user
    end

    it_behaves_like "public access to products"
    it_behaves_like "full access to products"
  end

  describe "guest access" do
    it_behaves_like "public access to products"

    describe "GET 'new'" do
      it "requires login"
    end
  end
end

favorite gems

| Comments

am compiling a list of gems that i use or will use in the future. this post will be regulary updated.

note: the following gems works with rails 4.0

hit the comments if you want something added

creating rails app from scratch

| Comments

this is a trick i always use when i create a new project and i don’t have rails gem installed globally. but before i put the commands, here’s my current setup:

1
2
3
4
5
$ gem list

*** LOCAL GEMS ***
bundler (1.3.5)
rbenv-gem-rehash (1.0.0)

also, i install the gems in a vendor directory inside the project; which means you need to specify the --path vendor when running bundle install.

here are the commands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ mkdir appname
$ cd appname
$ echo "source 'https://rubygems.org'" > Gemfile
$ echo "gem 'rails', '~> 4.0.0'" >> Gemfile

# make sure that your ruby version is set before running the next commands. am
# using version 2.0.0.

$ bundle install --path vendor
$ bundle exec rails new . --skip-bundle -d mysql
$ bundle install --path vendor
$ rails -v
$ bundle package
$ echo 'vendor/ruby' >> .gitignore

‘git… what is ____?’

| Comments

some git notes i put together for reference

Rollback changes

  • undo last commit, put changes to staging
1
$ git reset --soft HEAD^
  • new message, change the last commit
1
$ git commit --amend -m
  • undo last commit and all changes
1
$ git reset --hard HEAD^
  • undo 2 commits and all changes
1
$ git reset --hard HEAD^^

Remotes

  • add new remotes
1
$ git remote add <name> <address>
  • remove remotes
1
$ git remote rm <name>
  • push to remotes
1
$ git push -u <name> <branch>
  • remote show
1
$ git remote show origin
  • clean-up deleted remote branches
1
$ git remote prune origin

Tagging

  • list all tags
1
$ git tag
  • checkout code commit
1
$ git checkout v0.0.1
  • add new tag
1
$ git tag -a v0.0.3 -m "version 0.0.3"
  • push new tags
1
$ git push --tags

first post

| Comments

hello world!

trying out gh-pages, and i setup octopress. long story short; after you make a post, just generate and deploy your blog to Github via rake commands.

post and pages are written using markdown syntax. so far it’s all good.

if you use zsh, you might want to check this link.