View
Posted about 4 years ago. Visible to the public.

Enabling additional security features in Rails LTS

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:

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

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

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

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

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

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: Escape HTML entities in JSON

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 an user:

Copy
?> 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.

Copy
?> 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:

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

Option: Merge scopes safely (3.2 LTS only)

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:

Copy
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:

Copy
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 (all versions)

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:

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

Option: Disable JSON params parsing (all versions)

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:

Copy
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 (all versions)

This mitigates a possible security issue detailed in "Unsafe Query Risk in Active Record".

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:

Copy
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

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

or

Copy
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:

Copy
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.

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

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

    or

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

Author of this card:

Avatar
Thomas Eisenbarth
Last edit:
almost 2 years ago
by Henning Koch
Keywords:
configuring
13 cards