Allow Mass Assignment Rails Library

Early in 2012, a developer, named Egor Homakov, took advantage of a security hole at Github (a Rails app) to gain commit access to the Rails project.

His intent was mostly to point out a common security issue with many Rails apps that results from a feature, known as mass assignment (and did so rather loudly). In this article, we'll review what mass assignment is, how it can be a problem, and what you can do about it in your own applications.


What is Mass Assignment?

To begin, let's first take a look at what mass assignment means, and why it exists. By way of an example, imagine that we have the following class in our application:

# Assume the following fields: [:id, :first, :last, :email] class User < ActiveRecord::Base end

Mass assignment allows us to set a bunch of attributes at once:

attrs = {:first => "John", :last => "Doe", :email => "john.doe@example.com"} user = User.new(attrs) user.first #=> "John" user.last #=> "Doe" user.email #=> "john.doe@example.com"

Without the convenience of mass assignment, we'd have to write an assignment statement for each attribute to achieve the same result. Here's an example:

attrs = {:first => "John", :last => "Doe", :email => "john.doe@example.com"} user = User.new user.first = attrs[:first] user.last = attrs[:last] user.email = attrs[:email] user.first #=> "John" user.last #=> "Doe" user.email #=> "john.doe@example.com"

Obviously, this can get tedious and painful; so we bow at the feet of laziness and say, yes yes, mass assignment is a good thing.


The (Potential) Problem With Mass Assignment

One problem with sharp tools is that you can cut yourself with them.

But wait! One problem with sharp tools is that you can cut yourself with them. Mass assignment is no exception to this rule.

Suppose now that our little imaginary application has acquired the ability to fire missiles. As we don't want the world to turn to ash, we add a boolean permission field to the model to decide who can fire missiles.

class AddCanFireMissilesFlagToUsers < ActiveRecord::Migration def change add_column :users, :can_fire_missiles, :boolean, :default => false end end

Let's also assume that we have a way for users to edit their contact information: this might be a form somewhere that is accessible to the user with text fields for the user's first name, last name, and email address.

Our friend John Doe decides to change his name and update his email account. When he submits the form, the browser will issue a request similar to the following:

PUT http://missileapp.com/users/42?user[first]=NewJohn&user[email]=john.doe@newemail.com

The action within the might look something like:

def update user = User.find(params[:id]) if user.update_attributes(params[:user]) # Mass assignment! redirect_to home_path else render :edit end end

Given our example request, the hash will look similar to:

{:id => 42, :user => {:first => "NewJohn", :email => "john.doe@newemail.com"} # :id - parsed by the router # :user - parsed from the incoming querystring

Now let's say that NewJohn gets a little sneaky. You don't necessarily need a browser to issue an HTTP request, so he writes a script that issues the following request:

PUT http://missileapp.com/users/42?user[can_fire_missiles]=true

Fields, like , , and , are quite easily guessable.

When this request hits our action, the call will see , and give NewJohn the ability to fire missiles! Woe has become us.

This is exactly how Egor Homakov gave himself commit access to the Rails project. Because Rails is so convention-heavy, fields like , , and are quite easily guessable. Further, if there aren't protections in place, you can gain access to things that you're not supposed to be able to touch.


How to Deal With Mass Assignment

So how do we protect ourselves from wanton mass assignment? How do we prevent the NewJohns of the world from firing our missiles with reckless abandon?

Luckily, Rails provides a couple tools to manage the issue: and .

: The BlackList

Using , you can specify which fields may never be mass-ly assignable:

class User < ActiveRecord::Base attr_protected :can_fire_missiles end

Now, any attempt to mass-assign the attribute will fail.

: The WhiteList

The problem with is that it's too easy to forget to add a newly implemented field to the list.

This is where comes in. As you might have guessed, it's the opposite of : only list the attributes that you want to be mass-assignable.

As such, we can switch our class to this approach:

class User < ActiveRecord::Base attr_accessible :first, :last, :email end

Here, we're explicitly listing out what can be mass-assigned. Everything else will be disallowed. The advantage here is that if we, say, add an flag to the model, it will automatically be safe from mass-assignment.

As a general rule, you should prefer to , as it helps you err on the side of caution.

Mass Assignment Roles

Rails 3.1 introduced the concept of mass-assignment "roles". The idea is that you can specify different and lists for different situations.

class User < ActiveRecord::Base attr_accessible :first, :last, :email # :default role attr_accessible :can_fire_missiles, :as => :admin # :admin role end user = User.new({:can_fire_missiles => true}) # uses the :default role user.can_fire_missiles #=> false user2 = User.new({:can_fire_missiles => true}, :as => :admin) user.can_fire_missiles #=> true

Application-wide Configuration

You can control mass assignment behavior in your application by editing the setting within the file.

If set to , mass assignment protection will only be activated for the models where you specify an or list.

If set to , mass assignment will be impossible for all models unless they specify an or list. Please note that this option is enabled by default from Rails 3.2.3 forward.

Strictness

Beginning with Rails 3.2, there is additionally a configuration option to control the strictness of mass assignment protection: .

If set to , it will raise an any time that your application attempts to mass-assign something it shouldn't. You'll need to handle these errors explicitly. As of v3.2, this option is set for you in the development and test environments (but not production), presumably to help you track down where mass-assignment issues might be.

If not set, it will handle mass-assignment protection silently - meaning that it will only set the attributes it's supposed to, but won't raise an error.


Rails 4 Strong Parameters: A Different Approach

Mass assignment security is really about handling untrusted input.

The Homakov Incident initiated a conversation around mass assignment protection in the Rails community (and onward to other languages, as well); an interesting question was raised: does mass assignment security belong in the model layer?

Some applications have complex authorization requirements. Trying to handle all special cases in the model layer can begin to feel clunky and over-complicated, especially if you find yourself plastering all over the place.

A key insight here is that mass assignment security is really about handling untrusted input. As a Rails application receives user input in the controller layer, developers began wondering whether it might be better to deal with the issue there instead of ActiveRecord models.

The result of this discussion is the Strong Parameters gem, available for use with Rails 3, and a default in the upcoming Rails 4 release.

Assuming that our missile application is bult on Rails 3, here's how we might update it for use with the stong parameters gem:

Add the gem

Add the following line to the Gemfile:

gem strong_parameters

Turn off model-based mass assignment protection

Within :

config.active_record.whitelist_attributes = false

Tell the models about it

class User < ActiveRecord::Base include ActiveModel::ForbiddenAttributesProtection end

Update the controllers

class UsersController < ApplicationController def update user = User.find(params[:id]) if user.update_attributes(user_params) # see below redirect_to home_path else render :edit end end private # Require that :user be a key in the params Hash, # and only accept :first, :last, and :email attributes def user_params params.require(:user).permit(:first, :last, :email) end end

Now, if you attempt something like , you'll get an error in your application. You must first call on the hash with the keys that are allowed for a specific action.

The advantage to this approach is that you must be explicit about which input you accept at the point that you're dealing with the input.

Note: If this was a Rails 4 app, the controller code is all we'd need; the strong parameters functionality will be baked in by default. As a result, you won't need the include in the model or the separate gem in the Gemfile.


Wrapping Up

Mass assignment can be an incredibly useful feature when writing Rails code. In fact, it's nearly impossible to write reasonable Rails code without it. Unfortunately, mindless mass assignment is also fraught with peril.

Hopefully, you're now equipped with the necessary tools to navigate safely in the mass assignment waters. Here's to fewer missiles!

Last revision (mm/dd/yy): 01/18/2018

Introduction

This Cheatsheet intends to provide quick basic Ruby on Rails security tips for developers. It complements, augments or emphasizes points brought up in the rails security guide from rails core. The Rails framework abstracts developers from quite a bit of tedious work and provides the means to accomplish complex tasks quickly and with ease. New developers, those unfamiliar with the inner-workings of Rails, likely need a basic set of guidelines to secure fundamental aspects of their application. The intended purpose of this doc is to be that guide.

Items

Command Injection

Ruby offers a function called “eval” which will dynamically build new Ruby code based on Strings. It also has a number of ways to call system commands.

1 eval("ruby code here")2 System("os command here")3 `ls -al /`(backtickscontainoscommand)4 Kernel.exec("os command here")5 open("| os command here")

While the power of these commands is quite useful, extreme care should be taken when using them in a Rails based application. Usually, its just a bad idea. If need be, a whitelist of possible values should be used and any input should be validated as thoroughly as possible. The Ruby Security Reviewer's Guide has a section on injection and there are a number of OWASP references for it, starting at the top: Command Injection.

SQL Injection

Ruby on Rails is often used with an ORM called ActiveRecord, though it is flexible and can be used with other data sources. Typically very simple Rails applications use methods on the Rails models to query data. Many use cases protect for SQL Injection out of the box. However, it is possible to write code that allows for SQL Injection.

Here is an example (Rails 2.X style):

1 @projects=Project.find(:all,:conditions=>“namelike#{params[:name]}”)

A Rails 3.X example:

1 name=params[:name]2 @projects=Project.where(“namelike‘“+name+“‘“);

In both of these cases, the statement is injectable because the name parameter is not escaped.

Here is the idiom for building this kind of statement:

1 @projects=Project.find(:all,:conditions=>[“namelike?”,“#{params[:name]}”] )

An AREL based solution:

1 @projects=Project.where("name like ?","%#{params[:name]}%")

Use caution not to build SQL statements based on user controlled input. A list of more realistic and detailed examples is here: rails-sqli.org. OWASP has extensive information about SQL Injection.

Cross-site Scripting (XSS)

By default, in Rails 3.0 and up protection against XSS comes as the default behavior. When string data is shown in views, it is escaped prior to being sent back to the browser. This goes a long way, but there are common cases where developers bypass this protection - for example to enable rich text editing. In the event that you want to pass variables to the front end with tags intact, it is tempting to do the following in your .erb file (ruby markup).

1 <%= raw @product.name %> # - Wrong! Older rails versions, do not do this! 2 <%=@product.name.html_safe%> # - Wrong! Newer rails versions, do not do this!3 <%= content_tag @product.name %># - Wrong! Newer rails versions, do not do this!

Unfortunately, any field that uses raw, html_safe, content_tag or similar like this will be a potential XSS target. Note that there are also widespread misunderstandings about html_safe. This writeup describes the underlying SafeBuffer mechanism in detail. Other tags that change the way strings are prepared for output can introduce similar issues, including content_tag.

1 content_tag("/><script>alert('hack!');</script>")# XSS example2 # produces: </><script>alert('hack!');</script>><//><script>alert('hack!');</script>>

The method html_safe() of String is somewhat confusingly named. It means that we know for sure the content of the string is safe to include in HTML without escaping. This method itself is un-safe!

If you must accept HTML content from users, consider a markup language for rich text in an application (Examples include: markdown and textile) and disallow HTML tags. This helps ensures that the input accepted doesn’t include HTML content that could be malicious. If you cannot restrict your users from entering HTML, consider implementing content security policy to disallow the execution of any javascript. And finally, consider using the #sanitize method that let's you whitelist allowed tags. Be careful, this method has been shown to be flawed numerous times and will never be a complete solution.

An often overlooked XSS attack vector for older versions of rails is the href value of a link:

1 <%=link_to“PersonalWebsite”,@user.website%>

If @user.website contains a link that starts with “javascript:”, the content will execute when a user clicks the generated link:

1 <ahref=”javascript:alert(‘Haxored’)”>PersonalWebsite</a>

Newer Rails versions escape such links in a better way.

1 link_to"Personal Website",'javascript:alert(1);'.html_safe()2 # => "<a href=\"javascript:alert(1);\">Personal Website</a>"

Using Content Security Policy is one more security measure to forbid execution for links starting with javascript: .

Brakeman scanner helps in finding XSS problems in Rails apps.

OWASP provides more general information about XSS in a top level page: Cross-site Scripting (XSS).

Sessions

By default, Ruby on Rails uses a Cookie based session store. What that means is that unless you change something, the session will not expire on the server. That means that some default applications may be vulnerable to replay attacks. It also means that sensitive information should never be put in the session.

The best practice is to use a database based session, which thankfully is very easy with Rails:

1 Project::Application.config.session_store:active_record_store

There is an OWASP Session Management Cheat Sheet.

Authentication

As with all sensitive data, start securing your authentication with enabling TLS in your configuration:

1 # config/environments/production.rb2 # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies3 config.force_ssl=true

Uncomment the line 3 as above in your configuration.

Generally speaking, Rails does not provide authentication by itself. However, most developers using Rails leverage libraries such as Devise or AuthLogic to provide authentication.

To enable authentication it is possible to use Devise gem.

Install it using:

gem 'devise'

Then install it to the user model:

rails generate devise:install

Next, specify which resources (routes) require authenticated access in routes:

1 Rails.application.routes.drawdo 2 authenticate:userdo 3 resources:somethingdo# these resource require authentication 4 ... 5 end 6 end 7 8 devise_for:users# sign-up/-in/out routes 9 10 rootto:'static#home'# no authentication required11 12 end

To enforce password complexity, it is possible to use zxcvbn gem. Configure your user model with it:

1 classUser<ApplicationRecord2 devise:database_authenticatable,3 # other devise features, then4 :zxcvbnable5 end

And configure the required password complexity:

1 # in config/initializers/devise.rb2 Devise.setupdo|config|3 # zxcvbn score for devise4 config.min_password_score=4# complexity score here.5 ...

You can try out this PoC, to learn more about it.


Next, omniauth gem allows for multiple strategies for authentication. Using it one can configure secure authentication with Facebook, LDAP and many other providers. Read on here.

Token Authentication

Devise usually uses Cookies for authentication.

In the case token authentication is wished instead, it could be implemented with a gem devise_token_auth.

It supports multiple front end technologies, for example angular2-token.

This gem is configured similar to the devise gem itself. It also requires omniauth as a dependency.

# token-based authentication gem 'devise_token_auth' gem 'omniauth'

Then a route is defined:

mount_devise_token_auth_for 'User', at: 'auth'

And the User model is modified accordingly.

These actions can be done with one command:

rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]

You may need to edit the generated migration to avoid unnecessary fields and/or field duplication depending on your use case.

Note: when you use only token authentication, there is no more need in CSRF protection in controllers. If you use both ways: cookies and tokens, the paths where cookies are used for authentication still must be protected from forgery!


There is an OWASP Authentication Cheat Sheet.

Insecure Direct Object Reference or Forceful Browsing

By default, Ruby on Rails apps use a RESTful uri structure. That means that paths are often intuitive and guessable. To protect against a user trying to access or modify data that belongs to another user, it is important to specifically control actions. Out of the gate on a vanilla Rails application, there is no such built in protection. It is possible to do this by hand at the controller level.

It is also possible, and probably recommended, to consider resource-based access control libraries such as cancancan (cancan replacement) or punditto do this. This ensures that all operations on a database object are authorized by the business logic of the application.

More general information about this class of vulnerability is in the OWASP Top 10 Page.

CSRF (Cross Site Request Forgery)

Ruby on Rails has specific, built in support for CSRF tokens. To enable it, or ensure that it is enabled, find the base ApplicationController and look for a directive such as the following:

1 classApplicationController<ActionController::Base2 protect_from_forgery

Note that the syntax for this type of control includes a way to add exceptions. Exceptions may be useful for API’s or other reasons - but should be reviewed and consciously included. In the example below, the Rails ProjectController will not provide CSRF protection for the show method.

1 classProjectController<ApplicationController2 protect_from_forgery:except=>:show

Also note that by default Rails does not provide CSRF protection for any HTTP GET request.

Note: if you use token authentication only, there is no need to protect from CSRF in controllers like this. If cookie-based authentication is used on some paths, then the protections is still required on them.

There is a top level OWASP page for Cross-Site Request Forgery (CSRF).

Mass Assignment and Strong Parameters

Although the major issue with Mass Assignment has been fixed by default in base Rails specifically when generating new projects, it still applies to older and upgraded projects so it is important to understand the issue and to ensure that only attributes that are intended to be modifiable are exposed.

When working with a model, the attributes on the model will not be accessible to forms being posted unless a programmer explicitly indicates that:

1 classProject<ActiveRecord::Base2 attr_accessible:name,:admin3 end

With the admin attribute accessible based on the example above, the following could work:

curl -d “project[name]=triage&project[admin]=1” host:port/projects

Review accessible attributes to ensure that they should be accessible. If you are working in Rails < 3.2.3 you should ensure that your attributes are whitelisted with the following:

1 config.active_record.whitelist_attributes=true

In Rails 4.0 strong parameters will be the recommended approach for handling attribute visibility. It is also possible to use the strong_parameters gem with Rails 3.x, and the strong_parameters_rails2 gem for Rails 2.3.x applications.

Redirects and Forwards

Web applications often require the ability to dynamically redirect users based on client-supplied data. To clarify, dynamic redirection usually entails the client including a URL in a parameter within a request to the application. Once received by the application, the user is redirected to the URL specified in the request. For example:

http://www.example.com/redirect?url=http://www.example_commerce_site.com/checkout

The above request would redirect the user to http://www.example.com/checkout. The security concern associated with this functionality is leveraging an organization’s trusted brand to phish users and trick them into visiting a malicious site, in our example, “badhacker.com”. Example:

http://www.example.com/redirect?url=http://badhacker.com

The most basic, but restrictive protection is to use the :only_path option. Setting this to true will essentially strip out any host information. However, the :only_path option must be part of the first argument. If the first argument is not a hash table, then there is no way to pass in this option. In the absence of a custom helper or whitelist, this is one approach that can work:

1 begin2 ifpath=URI.parse(params[:url]).path3 redirect_topath4 end5 rescueURI::InvalidURIError6 redirect_to'/'7 end

If matching user input against a list of approved sites or TLDs against regular expression is a must, it makes sense to leverage a library such as URI.parse() to obtain the host and then take the host value and match it against regular expression patterns. Those regular expressions must, at a minimum, have anchors or there is a greater chance of an attacker bypassing the validation routine.

Example:

1 require‘uri’2 host=URI.parse(“#{params[:url]}”).host3 validation_routine(host)ifhost# this can be vulnerable to javascript://trusted.com/%0Aalert(0) so check .scheme and .port too4 defvalidation_routine(host)5 # Validation routine where we use \A and \z as anchors *not* ^ and $6 # you could also check the host value against a whitelist7 end


Also blind redirecting to user input parameter can lead to XSS. Example:

1 redirect_toparams[:to]
http://example.com/redirect?to[status]=200&to[protocol]=javascript:alert(0)//

The obvious fix for this type of vulnerability is to restrict to specific Top-Level Domains (TLDs), statically define specific sites, or map a key to it’s value. Example:

1 ACCEPTABLE_URLS={2 ‘our_app_1’=>“https://www.example_commerce_site.com/checkout”,3 ‘our_app_2’=>“https://www.example_user_site.com/change_settings”4 }

http://www.example.com/redirect?url=our_app_1

1 defredirect2 url=ACCEPTABLE_URLS[“#{params[:url]}”]3 redirect_tourlifurl4 end

There is a more general OWASP resource about unvalidated redirects and forwards.

Dynamic Render Paths

In Rails, controller actions and views can dynamically determine which view or partial to render by calling the “render” method. If user input is used in or for the template name, an attacker could cause the application to render an arbitrary view, such as an administrative page.

Care should be taken when using user input to determine which view to render. If possible, avoid any user input in the name or path to the view.

Cross Origin Resource Sharing

Occasionally, a need arises to share resources with another domain. For example, a file-upload function that sends data via an AJAX request to another domain. In these cases, the same-origin rules followed by web browsers must be bent. Modern browsers, in compliance with HTML5 standards, will allow this to occur but in order to do this; a couple precautions must be taken.

When using a nonstandard HTTP construct, such as an atypical Content-Type header, for example, the following applies:

The receiving site should whitelist only those domains allowed to make such requests as well as set the Access-Control-Allow-Origin header in both the response to the OPTIONS request and POST request. This is because the OPTIONS request is sent first, in order to determine if the remote or receiving site allows the requesting domain. Next, a second request, a POST request, is sent. Once again, the header must be set in order for the transaction to be shown as successful.

When standard HTTP constructs are used:

The request is sent and the browser, upon receiving a response, inspects the response headers in order to determine if the response can and should be processed.

Whitelist in Rails:

Gemfile

gem 'rack-cors', :require => 'rack/cors'

config/application.rb

module Sample class Application < Rails::Application config.middleware.use Rack::Cors do allow do origins 'someserver.example.com' resource %r{/users/\d+.json},  :headers => ['Origin', 'Accept', 'Content-Type'],  :methods => [:post, :get] end end end end

To set a header value, simply access the response.headers object as a hash inside your controller (often in a before/after_filter).

response.headers['X-header-name'] = 'value'

Rails 4 provides the "default_headers" functionality that will automatically apply the values supplied. This works for most headers in almost all cases.

ActionDispatch::Response.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1;' }

Strict transport security is a special case, it is set in an environment file (e.g. production.rb)

config.force_ssl = true

For those not on the edge, there is a library (secure_headers) for the same behavior with content security policy abstraction provided. It will automatically apply logic based on the user agent to produce a concise set of headers.

Business Logic Bugs

Any application in any technology can contain business logic errors that result in security bugs. Business logic bugs are difficult to impossible to detect using automated tools. The best ways to prevent business logic security bugs are to do code review, pair program and write unit tests.

Attack Surface

Generally speaking, Rails avoids open redirect and path traversal types of vulnerabilities because of its /config/routes.rb file which dictates what URL’s should be accessible and handled by which controllers. The routes file is a great place to look when thinking about the scope of the attack surface. An example might be as follows:

match ':controller(/:action(/:id(.:format)))' # this is an example of what NOT to do

In this case, this route allows any public method on any controller to be called as an action. As a developer, you want to make sure that users can only reach the controller methods intended and in the way intended.

Sensitive Files

Many Ruby on Rails apps are open source and hosted on publicly available source code repositories. Whether that is the case or the code is committed to a corporate source control system, there are certain files that should be either excluded or carefully managed.

/config/database.yml - May contain production credentials. /config/initializers/secret_token.rb - Contains a secret used to hash session cookie. /db/seeds.rb - May contain seed data including bootstrap admin user. /db/development.sqlite3 - May contain real data.

Encryption

Rails uses OS encryption. Generally speaking, it is always a bad idea to write your own encryption.

Devise by default uses bcrypt for password hashing, which is an appropriate solution. Typically, the following config causes the 10 stretches for production: /config/initializers/devise.rb

config.stretches = Rails.env.test? ? 1 : 10

Updating Rails and Having a Process for Updating Dependencies

In early 2013, a number of critical vulnerabilities were identified in the Rails Framework. Organizations that had fallen behind current versions had more trouble updating and harder decisions along the way, including patching the source code for the framework itself.

An additional concern with Ruby applications in general is that most libraries (gems) are not signed by their authors. It is literally impossible to build a Rails based project with libraries that come from trusted sources. One good practice might be to audit the gems you are using.

In general, it is important to have a process for updating dependencies. An example process might define three mechanisms for triggering an update of response:

  • Every month/quarter dependencies in general are updated.
  • Every week important security vulnerabilities are taken into account and potentially trigger an update.
  • In EXCEPTIONAL conditions, emergency updates may need to be applied.

Tools

Use brakeman, an open source code analysis tool for Rails applications, to identify many potential issues. It will not necessarily produce comprehensive security findings, but it can find easily exposed issues. A great way to see potential issues in Rails is to review the brakeman documentation of warning types.

There are emerging tools that can be used to track security issues in dependency sets, like https://appcanary.com/ and https://gemnasium.com/.

Another area of tooling is the security testing tool Gauntlt which is built on cucumber and uses gherkin syntax to define attack files.

Launched in May 2013 and very similiar to brakeman scanner, the dawnscanner rubygem is a static analyzer for security issues that work with Rails, Sinatra and Padrino web applications. Version 0.60 has more than 30 ruby specific CVE security checks and future releases custom checks against Cross Site Scripting and SQL Injections will be added.

Authors and Primary Editors

Matt Konda - mkonda [at] jemurai.com
Neil Matatall neil [at] matatall.com
Ken Johnson cktricky [at] gmail.com
Justin Collins justin [at] presidentbeef.com
Jon Rose - jrose400 [at] gmail.com
Lance Vaughn - lance [at] cabforward.com
Jon Claudius - jonathan.claudius [at] gmail.com
Jim Manico jim [at] owasp.org
Aaron Bedra aaron [at] aaronbedra.com
Egor Homakov homakov [at] gmail.com
Zaur Molotnikov qutorial [at] gmail.com 🡕 profile

Related Articles and References

Other Cheatsheets

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *