Rails 6.1 SQL Injection Updates
Since early 2013, I have been maintaining rails-sqli.org, a collection of Rails ActiveRecord methods that can be vulnerable to SQL injection.
Rails 6 has been out since December 2019, but sadly the site has been missing information about changes and new methods in Rails 6.
As that deficiency has recently been rectified, let’s walk through what has changed since Rails 5!
delete_all
, destroy_all
In earlier versions of Rails, delete_all
and destroy_all
could be passed a string of raw SQL.
In Rails 6, these two methods no longer accept any arguments.
Instead, you can use…
delete_by
, destroy_by
New in Rails 6, delete_by
and destroy_by
accept the same type of arguments as where
: a Hash, an Array, or a raw SQL String.
This means they are vulnerable to the same kind of SQL injection.
For example:
params[:id] = "1) OR 1=1--"
User.delete_by("id = #{params[:id]}")
Resulting query that deletes all users:
DELETE FROM "users" WHERE (id = 1) OR 1=1--)
order
, reorder
Prior to Rails 6, it was possible to pass arbitrary SQL to the order
and reorder
methods.
Since Rails did not offer an easy way of setting sort direction, this kind of code was common:
User.order("name #{params[:direction]}")
In Rails 6.0, injection attempts would raise a deprecation warning:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "--". Non-attribute arguments will be disallowed in Rails 6.1. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
Starting with Rails 6.1, some logic to check the arguments to order
. If the arguments do not appear to be column names or sort order, they will be rejected:
> User.order("name ARGLBARGHL")
Traceback (most recent call last):
1: from (irb):12
ActiveRecord::UnknownAttributeReference (Query method called with non-attribute argument(s): "name ARGLBARGHL")
It is still possible to inject additional columns to extract some information from the table, such as number of columns or names of the columns:
params[:direction] = ", 8"
User.order("name #{params[:direction]}")
Resulting exception:
ActiveRecord::StatementInvalid (SQLite3::SQLException: 2nd ORDER BY term out of range - should be between 1 and 7)
pluck
pluck
pulls out specified columns from a query, instead of loading whole records.
In previous versions of Rails, pluck
(somewhat surprisingly!) accepted arbitrary SQL strings if they were passed in as an array.
Like order
/reorder
, Rails 6.0 started warning about this:
> User.pluck(["1"])
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): ["1"]. Non-attribute arguments will be disallowed in Rails 6.1. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
In Rails 6.1, pluck
now only accepts attribute names!
reselect
Rails 6 introduced reselect
, which allows one to completely replace the SELECT
clause of a query. Like select
, it accepts any SQL string. Since SELECT
is at the very beginning of the SQL query, it makes it a great target for SQL injection.
params[:column] = "* FROM orders -- "
User.select(:name).reselect(params[:column])
Note this selects all columns from a different table:
SELECT * FROM orders -- FROM "users"
rewhere
rewhere
is analogous to reselect
but it replaces the WHERE
clause.
Like where
, it is very easy to open up rewhere
to SQL injection.
params[:age] = "1=1) OR 1=1--"
User.where(name: "Bob").rewhere("age > #{params[:age]}")
Resulting query:
SELECT "users".* FROM "users" WHERE "users"."name" = ? AND (age > 1=1) OR 1=1--)
Wrapping Up
Any other new methods that allow SQL injection? Let me know!
Want to find out more?
- rails-sqli.org for the complete list.
- Brakeman to help find vulnerable queries in your code.
- OWASP SQL Injection Cheat Sheet to learn more about preventing SQL injection.
Another Reason to Avoid constantize in Rails
Backstory
Recently, a friend asked me if just calling constantize
on user input was dangerous, even if subsequent code did not use the result:
params[:class].classify.constantize
Brakeman generates a “remote code execution” warning for this code:
Confidence: High Category: Remote Code Execution Check: UnsafeReflection Message: Unsafe reflection method `constantize` called with parameter value Code: params[:class].classify.constantize File: app/controllers/users_controller.rb Line: 7
But why? Surely just converting a string to a constant (if the constant even exists!) can’t be dangerous, right?
Coincidentally, around that same time I was looking at Ruby deserialization gadgets - in particular this one which mentions that Ruby’s Digest
module will load a file based on the module name. For example, Digest::A
will try to require 'digest/a'
:
2.7.0 :001 > require 'digest' => true 2.7.0 :002 > Digest::Whatever Traceback (most recent call last): 5: from /home/justin/.rvm/rubies/ruby-2.7.0/bin/irb:23:in `<main>' 4: from /home/justin/.rvm/rubies/ruby-2.7.0/bin/irb:23:in `load' 3: from /home/justin/.rvm/rubies/ruby-2.7.0/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>' 2: from (irb):2 1: from /home/justin/.rvm/rubies/ruby-2.7.0/lib/ruby/2.7.0/digest.rb:16:in `const_missing' LoadError (library not found for class Digest::Whatever -- digest/whatever)
The Digest
library uses the const_missing
hook to implement this functionality.
This made me wonder if constantize
and const_missing
could be connected, and what the consequences would be.
Constantizing in Rails
The constantize
method in Rails turns a string into a constant. If the constant does not exist then a NameError
will be raised.
However, it is possible to hook into the constant lookup process in Ruby by defining a const_missing
method. If a constant cannot be found in a given module, and that module has const_missing
defined, then const_missing
will be invoked.
2.7.0 :001 > module X 2.7.0 :002 > def self.const_missing(name) 2.7.0 :003 > puts "You tried to load #{name.inspect}" 2.7.0 :004 > end 2.7.0 :005 > end => :const_missing 2.7.0 :006 > X::Hello You tried to load :Hello => nil
If const_missing
is implemented with behavior based on the constant name, such as loading a file or creating a new object, there is an opportunity for malicious behavior.
Some Vulnerable Gems
Fortunately, const_missing
is not used very often. When it is, the implementation is not usually exploitable.
Searching across ~1300 gems, I found only ~40 gems with a const_missing
implementation.
Of those, the majority were not exploitable because they checked the constant name against expected values or called const_get
which raises an exception if the constant does not exist.
One gem, coderay, loads files based on constant names like the Digest library. Also like the Digest library, this does not appear to be exploitable because the files are limited to a single coderay directory.
The next two gems below have memory leaks, which can enable denial of service attacks through memory exhaustion.
Temple
The Temple gem is a foundational gem used by Haml, Slim, and other templating libraries.
In Temple, there is a module called Temple::Mixins::GrammarDSL
that implements const_missing
like this:
def const_missing(name)
const_set(name, Root.new(self, name))
end
The method creates a new constant based on the given name
and assigns a new object.
This is a memory leak since constants are never garbage collected. If an attacker can trigger it, they can create an unlimited number of permanent objects, using up as much memory as possible.
Unfortunately, it is easy to exploit this code.
Temple::Grammar
extends Template::Mixins::GrammarDSL
and is a core class for Temple. Let’s see if it is loaded by Haml, a popular templating library often used with Rails:
2.7.0 :001 > require 'haml' => true 2.7.0 :002 > Temple::Grammar => Temple::Grammar
Great! What happens if we try to reference a module that definitely does not exist?
2.7.0 :003 > Temple::Grammar::DefinitelyDoesNotExist => #<Temple::Mixins::GrammarDSL::Root:0x000055a79b011060 @grammar=Temple::Grammar, @children=[], @name=:DefinitelyDoesNotExist>
As can be seen above, the constant is created along with a new object.
To go one step further… does the use of constantize invoke this code?
We can test by loading a Rails console for an application using Haml:
Loading development environment (Rails 6.0.3.2) 2.7.0 :001 > require 'haml' => false 2.7.0 :002 > 'Temple::Grammar::DefinitelyDoesNotExist'.constantize => #<Temple::Mixins::GrammarDSL::Root:0x000055ba28031a50 @grammar=Temple::Grammar, @children=[], @name=:DefinitelyDoesNotExist>
It does!
Any Ruby on Rails application using Haml or Slim that calls constantize
on user input (e.g. params[:class].classify.constantize
) is vulnerable to a memory leak via this method.
Restforce
A very similar code pattern is implemented in the restforce gem.
The ErrorCode module uses const_missing
like this:
module ErrorCode
def self.const_missing(constant_name)
const_set constant_name, Class.new(ResponseError)
end
end
Nearly the same, except this actually creates new classes, not just regular objects.
We can verify again:
Loading development environment (Rails 6.0.3.2) 2.7.0 :001 > require 'restforce' => false 2.7.0 :002 > Restforce::ErrorCode::WhateverWeWant => Restforce::ErrorCode::WhateverWeWant
This time we get as many new classes as we want.
This has been fixed in Restforce 5.0.0.
Finding and Exploiting Memory Leaks
Finding vulnerable code like this in a production application would be difficult. You would need to guess which parameters might be constantize
d.
Verifying that you’ve found a memory leak is a little tricky and the two memory leaks described above create very minimal objects.
From what I could estimate, a new Rule
object in Temple uses about 300 bytes of memory, while a new class in Restforce was taking up almost 1,000 bytes.
Based on that and my testing, it would take 1 to 4 million requests to use just 1GB of memory.
Given that web applications are usually restarted on a regular basis and it’s not usually a big deal to kill off a process and start a new one, this does not seem particularly impactful.
However, it would be annoying and possibly harmful for smaller sites. For example, the base Heroku instance only has 512MB of memory.
Another note here: Memory leaks are not the worst outcome of an unprotected call to constantize
. More likely it can trigger remote code execution. The real issue I am trying to explore here is the unexpected behavior that may be hidden in dependencies.
Conclusions
In short: Avoid using constantize
in Rails applications. If you need to use it, check against an allowed set of class names before calling constantize
. (Calling classify
before checking is okay, though.)
Likewise for const_missing
in Ruby libraries. Doing anything dynamic with the constant name (loading files, creating new objects, evaluating code, etc.) should be avoided. Ideally, check against an expected list of names and reject anything else.
In the end, this comes down to the security basics of not trusting user input and strictly validating inputs.
Edit: It seems some language I used above was a little ambiguous, so I tweaked it. Calling classify
does not make the code safe - I meant calling classify
is not dangerous by itself. It’s the subsequent call to constantize
that is dangerous. So you can safely call classify
, check against a list of allowed classes, then take the appropriate action.
Why 'Escaping' JavaScript is Dangerous
A recent vulnerability report and the blog post behind it brought my attention back to the escape_javascript
Ruby on Rails helper method.
Let me say it again... if you are calling `escape_javascript` or `j` in your Rails code, please don't! https://t.co/60KLEjHX3T
— Justin Collins (@presidentbeef) May 9, 2020
It’s bad form to drop blanket statements without explanation or evidence, so here it is:
Escaping HTML
Part of the danger of escape_javascript
is the name and apparent relationship to html_escape
.
HTML is a markup language for writing documents. Therefore, it must have a method for representing itself in text.
In other words, there must be a way to encode <b>
such that the browser displays <b>
and does not interpret it as HTML.
As a result, HTML has a well-defined HTML encoding strategy. In the context of security and cross-site scripting, if a value output in an HTML context is HTML escaped, it is safe - the value will not be interpreted as HTML.
(See my post all about escaping!)
Escaping Javascript
On the other hand, JavaScript has no such escaping requirements or capabilities.
Therefore, the “escaping” performed by escape_javascript
is limited.
The vulnerability report states the method is for “escaping JavaScript string literals”.
In particular, escape_javascript
is only useful in one, single context: inside JavaScript strings!
For example:
# ERb Template
<script>
var x = '<%= escape_javascript some_value %>';
</script>
Use of escape_javascript
in any other context is incorrect and dangerous!
This is and always has been dangerous (note the missing quotes):
# ERb Template
<script>
var x = <%= escape_javascript some_value %>;
</script>
some_value
could be a payload like 1; do_something_shady(); //
which would result in the following HTML:
<script>
var x = 1; do_something_shady(); //;
</script>
The escape_javascript
helper does not and cannot make arbitrary values inserted into JavaScript “safe” in the same way html_escape
makes values safe for HTML.
CVE-2020-5267
Jesse’s post has more details, but here’s the gist: JavaScript added a new string literal. Instead of just single and double-quotes, now there are also backticks `
which support string interpolation (like Ruby!).
This meant it was simple to bypass escape_javascript
and execute arbitrary JavaScript by using a backtick to break out of the string or just #{...}
to execute code during interpolation.
For example, if this were our code:
# ERb Template
<script>
var x = `<%= escape_javascript some_value %>`;
</script>
Then if some_value
had a payload of ```; do_something_shady(); //``, the resulting HTML would be:
<script>
var x = ``; do_something_shady(); //`
</script>
This is because escape_javascript
was not aware of backticks for strings.
Dangers of Dynamic Code Generation
Let me say it again… using dynamic javascript under practically any circumstance is inviting trouble. It might be ok. I’d rather not have to worry about it. https://t.co/wnPy3OnkKI
— Shake, Oreo (@ndm) May 9, 2020
As I have talked about before, web applications are essentially poorly-defined compilers generating code with untrusted inputs. In the end, the server is just returning a mishmash of code for the browser to interpret.
However, directly trying to generate safe code in a Turing-complete language like JavaScript or Ruby via string manipulation is a risky game.
Methods like escape_javascript
make it tempting to do so because the name sounds like it will make the code safe.
If at all possible, avoid dynamic code generation!