Object#send considered harmful
by Yuji Yamamoto on February 7, 2016
This is an English translation of Object#send 有害論.
Let me show you how Object#send
and Object#public_send
make vulnerabilities on your app when using without care.
Maybe it’s not surprising to you if you know the behavior of the methods well,
but no one doesn’t seem to have written about it yet as long as I googled.
TL;DR
Object#send
and Object#public_send
are almost as risky as eval
and system
.
Avoid to pass strings from any external sources (such as users’ one) to their arguments.
Pass only hard-coded, and trusted method names (except when you create some special libraries for meta-programming).
Dangerous Case
For an obvious example, consider an action of a Rails app’s controller like this.
Haven’t you ever written a code like this?
def some_action
FooModel.find_by_id(params[:id]).send(params[:method])
...
end
If an attacker gives a string such as exit
, he/she can make the Rails app exit! 1
This is caused by calling Kernel
module’s exit
method.
The exit
method can be called by almost any objects in Ruby, as well as the model above.
So try to paste the code below onto your REPL (e.g. irb
, pry
).
1.send 'exit'
The REPL session exited right?
Why such a apparently useless thing can be done?
It’s because almost all Object
s in Ruby include
s Kernel
module.
Kernel
module has methods that can be called anywhere in your programs (so-called global functions),
such as exit
, puts
, and so on.
And Object
class include
s Kernel
module, which makes puts
and exit
available at almost any line of Ruby programs.
Consequently, if you write a code which passes arbitrary strings to send
,
any methods accessible by Object
, including Kernel
module’s methods can be called,
which exposes a surprisingly big vulnerability.
Much More Dangerous Case
Attackers could call even more dangerous methods in worse cases
because Kernel
has very various features (possibly more than you know).
Consider you add one parameter to the action which called send
in the previous example.
def some_action
FooModel.find_by_id(params[:id]).send(params[:method], params[:arg])
...
end
Now “OS commands injection” and “Ruby code injection (Is this the right name?)” vulnerability has been exposed
because both system
and eval
are defined in Kernel
module.
In other words, arbitrary code can be executed by an attacker:
if the attacker gives eval
as params[:method]
and User.delete_all
as params[:arg]
,
he/she can erase all users from your DB.
Can public_send
avoid?
If you’re a Ruby-meta-programming hacker, you might think
“Okay, you mean the private
methods in Kernel
are risky, right?
Then how about public_send
, which can’t call private
methods?”
To tell the truth, I had actually assumed that just until I began to write this post.
Unfortunately, even Object
class can call a method as risky as them: instance_eval
.
It can execute arbitrary Ruby codes as Kernel#eval
, by passing a string as its second argument.
And instance_eval
is available by public_send
because its Object
’s public
method.
There can be more risks!
These examples are just some cases among many possibilities.
Other dangers can arise from some implementation of the receiver class of send
.
In addition, unnoticed pitfalls can be made
by modification to the standard libraries by your libraries, or your colleagues.
It’s a problem specific to Ruby, in which the users of the libraries (including standard ones) can easily alter them.
Measure
As mentioned in the last secion, risks can exist anywhere
when you permit any strings to be passed as the arguments of send
.
Maybe blacklisting is not sufficient in most cases.
So make a whitelist of methods you want to use to surely avoid the risks.
Giving only hard-coded Symbol
’s would be the easiest and most reliable measure.
I guess the most of the use cases of send
would be covered by it.
The behavior after the app exits depends on the runner middleware (e.g. Unicorn, Passenger).↩︎