104 Enabling additional security features in Rails LTS

Updated . Posted . Visible to the public.

This document describes how to configure Rails LTS and how to take advantage of its optional security features.

The default Rails LTS configuration (:compatible) has been built for maximum compatibility with the official Rails releases. We do however recommend the :hardened configuration, which includes improvements we believe to be reasonable defaults for increased security in most applications.

On Rails 2.3, to activate :hardened configuration, add the following to the Rails::Initializer block in your config/environment.rb:

config.rails_lts_options = { :default => :hardened }

On Rails 3.2, Rails 4.2, add the same into config/application.rb:

config.rails_lts_options = { :default => :hardened }

If you'd like to stick with the :compatible default, use this line instead:

config.rails_lts_options = { :default => :compatible }

On Rails 5.2 and 6.1, there are currently no hardening settings necessary, so you do not need to configure this.

If you'd like to cherry-pick from the security features that are available, see below.

Cherry-picking security features

Below you will find a detailed list of the options security features in Rails LTS.

All of these features are enabled if you configure Rails LTS in :hardened mode (see above). If you'd prefer to opt into individual security features, while leaving others disabled, you will also find instructions below.

Option: Strings in polymorphic helpers (2.3 LTS, 3.2 LTS, 4.2 LTS)

By default, Rails LTS does not allow for strings to be used in polymorphic path helpers, e.g. url_for(['edit', @user]) is not allowed. Symbols are fine.

This is a potentially breaking change, and so you can opt out by setting allow_strings_for_polymorphic_paths: true.

See this card for more information.

You can enable this with

config.rails_lts_options = { :allow_strings_for_polymorphic_paths => false, ... }

Option: Escape HTML entities in JSON (2.3 LTS, 3.2 LTS)

By default, Rails does not escape HTML control character (like < or >) when serializing an object to JSON with to_json. This can lead to XSS security issues if your application returns JSON to the client (usually via AJAX) and arbitrary input can placed into the JSON serialization by a user:

?> foo = ["<b>foo</b>"]
=> ["<b>bar</b>"]
>> 
?> ActiveSupport::JSON::Encoding.escape_html_entities_in_json
=> false
>> 
?> foo.to_json
=> "[\"<b>bar</b>\"]"

Luckily there is a configuration option that converts HTML control characters to a JSON escape sequence.

?> ActiveSupport::JSON::Encoding.escape_html_entities_in_json = true
=> true
>> 
?> foo.to_json
=> "[\"\\u003Cb\\u003Ebar\\u003C/b\\u003E\"]"

This sends a few more bytes over the connection, but results in the same structure once parsed.

By default, entity escaping is disabled in Rails LTS (as in vanilla Rails). If possible for your project, we recommend to enable it:

config.rails_lts_options = { :escape_html_entities_in_json => true, ... }

Option: Merge scopes safely (3.2 LTS)

In some versions of Rails, when you chain two scopes with hash conditions on the same attribute, the second scope will overwrite (and not join!) conditions from the first scope:

class Contract < ActiveRecord::Base
  named_scope :with_region, proc { |region_id| { :conditions => { :region_id => region_id } } }
end

>> Contract.with_region(5)
=> SELECT * FROM `contracts` WHERE (`contracts`.`region_id` = 5)

>> Contract.with_region(5).with_region(7)
=> SELECT * FROM `contracts` WHERE (`contracts`.`region_id` = 7)

Note how the last query did not select on region_id = 5 AND region_id = 7 as expected.

This can be a security issue when you use scopes to restrict what a controller can see and edit. See this card for more background on the issue.

Rails LTS comes with a configuration option that forces scope merges to always use conjunctions (condition1 AND condition2) and never override an existing conditions from further up the chain.

By default, safe merging is disabled in Rails LTS (as in vanilla Rails). If possible for your project, we recommend to enable it:

config.rails_lts_options = { :merge_scope_conditions_with_conjunctions => true, ... }

This security feature is only available in Rails 3.2 LTS. For Rails 2.3 a manual workaround is available.

Option: Disable XML params parsing (2.3 LTS, 3.2 LTS)

Rails is capable of parsing parameters sent by HTTP requests in XML format to be automatically converted to the params hash. In the past, this lead to security implications which is why we suggest to disable this feature unless your application depends on this feature.

By default, XML parsing is enabled in Rails LTS (as in vanilla Rails). If possible for your project, we recommend to disable it:

config.rails_lts_options = { :disable_xml_parsing => true, ... }

Option: Disable JSON params parsing (2.3 LTS, 3.2 LTS, 4.2 LTS)

Rails is capable of parsing parameters sent by HTTP requests in JSON format to be automatically converted to the params hash. In the past, this lead to security implications which is why we suggest to disable this feature unless your application depends on this feature.

By default, JSON parsing is enabled in Rails LTS (as in vanilla Rails). If possible for your project, we recommend to disable it:

config.rails_lts_options = { :disable_json_parsing => true, ... }

Note that this might not be possible if your project has an API that relies on automatic JSON parsing by Rails.

Option: Throw exception on ambiguous table names (2.3 LTS, 3.2 LTS, 4.2 LTS)

This mitigates a possible security issue detailed in "Unsafe Query Risk in Active Record" Show archive.org snapshot .

The Rails team did not provide a patch, because it was not possible to fix this in a way that does not risk breaking existing applications. Rails LTS provides an opt-in fix which you can enable like this:

config.rails_lts_options = { :strict_unambiguous_table_names => true, ... }

If this is set, all ambiguous queries will now raise an exception. Specifically, if you use conditions with a nested hash

Record.all(:conditions => { :records => { :column => "value" } })

or

Record.find_all_by_records(:column => "value")

or similar, and the Record model has a column called records, this will cause an exception. This is to avoid any ambiguity where a column name might inadvertently be used as a table name. It is not usually an issue, since column names are rarely plural.

This also fixes the related issue with join queries. For instance, the following code will now throw an exception if the Record model has a column joined_records:

Record.all(:joins => :joined_records, :conditions => { :joined_records => { :column => "value" } })

Resolving ambiguities

If you activate :strict_unambiguous_table_names and now get exceptions due to ambiguous queries, you can fix them by either

  1. not using nested hashes

  2. aliasing you joined tables, i.e.

    Record.all(:joins => "INNER JOIN joined_records AS joined", :conditions => { :joined => { :column => "value" } })
    
  3. opting out of this feature, by using

    config.rails_lts_options = { :default => :compatible }
    

    or

    config.rails_lts_options = { ... , :strict_unambiguous_table_names => false }
    

Option: Cast integers to strings for MySQL string column comparison (2.3 LTS, 3.2 LTS)

This mitigates a potential security issue detailed in Potential Query Manipulation with Common Rails Practises Show archive.org snapshot .

In MySQL, code like User.where(token: 0).first will return any user where token is a string that is not castable to a number (because MySQL casts "some-token" to the integer 0 and then performs the comparison. This is dangerous for requests with JSON or XML params (where params[:token] might be 0).

With this option enabled, Rails will attempt to cast numbers to strings when performing a comparison with a string column.

You can enable this with

config.rails_lts_options = { :cast_integers_on_mysql_string_columns => true, ... }

You can read our advisory for more details.

Thomas Eisenbarth
Last edit
Tobias Kraze
Keywords
configuring
License
Source code in this card is licensed under the MIT License.
Posted by Thomas Eisenbarth to Rails LTS documentation (2013-05-28 14:15)