Anatomy of an Exploit: An In-depth Look at the Rails YAML Vulnerability

Share this article

yaml_hacked
Exploits happens, and this month the Rails and Ruby communities have seen no shortage. From a major exploit in Rails to a slightly different Rubygems.org attack, there has never been a better time to brush up on software security. Maybe you’re wondering why these vulnerabilities happen in the first place, why they weren’t caught in the first place, or maybe you just want to know the specifics of this attack. We’ll start off by taking a look at the anatomy of a security exploit, and then dive into the gory details of the YAML issue.

Why Insecure Code Happens

No one intends to write insecure software. These vulnerabilities are bugs in the software that can be taken advantage of by others. Unlike a normal bug that will cause your software to not function as intended, a bug that opens up a security hole might still work fine for your task and never actually throw any errors. Often times this is due to side effects in your code. Let’s take a look at some code examples:

Unexpected Input

Let’s say you need to double a number for some reason, so you write a function like this:
def double(number)
  return number * 2
end
You test it out and it works fine:
double(10)
# => 20
While this isn’t an insecure function, it can be used in ways you didn’t intend. Check out what happens when we pass a string to the function:
double("foo")
# => "foofoo"
This isn’t what we wanted nor is it the original intention of the code. Regardless, it’s a side effect of our implementation, if you pass in a string the * operator will be called on it which duplicates the string. This isn’t actually opening up any security holes, but it just goes to show you how software intended to do one thing can sometimes have unintended abilities.

A Real Example

We’re going to look at all the elements that enabled the YAML attack to happen on Rails system. You should know that all of this information is publicly available already and in the hands of the “bad guys” The hope is this info helps you to write better code and make better decisions in the future. If you do discover a security vulnerability in a framework or platform, do the responsible thing and report it to the owners before making the information public. To understand the attack you need to understand YAML, let’s do a quick refresher.

YAML Refresher

YAML (YAML Ain’t Markup Language) is often used by Rubyists to store configuration files. The most famous yml file is probably the config/database.yml used in Rails and it looks like this:
development:
  adapter: postgresql
  encoding: utf8
  database: example_development
  pool: 5
  host: localhost
And can be read in using YAML::load_file
require 'yaml'
database_config = YAML::load_file('database.yml')

puts database_config["development"]
# => {"adapter"=>"postgresql", "encoding"=>"utf8", "database"=>"example_development", "pool"=>5, "host"=>"localhost"}
YAML isn’t just for files, it’s a serialization format like JSON. We can use it to pass complicated objects like strings, numbers, and arrays. There is even support to pass arbitrary user defined objects like User or Product. This is where we get into trouble.

Ruby Objects in YAML

YAML allows us to represent ruby objects directly, the best way to understand how it works is to see it in action. Let’s see how to build a simple Array. You could put this in the top of a .yml file and read it in:
--- !ruby/array:Array
  - jacket
  - sweater
When you parse this it should produce ["jacket", "sweater"]. But we’re not limited to simple Ruby objects like arrays and strings, we can build any class that is in our project like User if we want to:
--- !ruby/hash:User
email: richard@example.com
Now when we load this YAML formatted string in:
string = "--- !ruby/hash:Usern" +
"email: richard@example.com"

YAML::load(string)
 => #<User id: 1, email: "richard@example.com">
We get a User object. Essentially what Ruby is doing is taking all the attributes on the left such as “email” and applying them as values on the right to a new object as if it was a hash. Like this:
user = User.new
user["email"] = "richard@example.com"
This is desired functionality by the creators of YAML, since it gives developers the ability to write and read Ruby objects to disk, like an object database. Unfortunately, this is an often overlooked ability of YAML. Furthermore, this ability has an unintended side effect, much like being able to pass a string into our double() method.

A Vulnerable Object

You might be tempted to think that since we’re only creating objects that have to be defined on our servers, this object instantiation ability wouldn’t be too bad. Unfortunately, let’s see how attackers used these functions to be able to execute arbitrary code. To take advantage of this exploit, we need a class in our code that evaluates code either on create:
user = User.new
or when we’re setting values
user["email"] = "richard@example.com"
Since both “email” and “richard@example.com” are values we can manipulate through YAML that is the best place to look. In this case a vulnerable class ActionDispatch::Routing::RouteSet::NamedRouteCollection was found to be exploitable via @lian and announced via Rapid7. To understand how this class is exploitable, let’s try to run arbitrary code on the class directly. First we instantiate the class:
unsafe_object = ActionDispatch::Routing::RouteSet::NamedRouteCollection.new
Then make a value:
struct        = OpenStruct.new(defaults: {})
Now, we craft an exploit payload foo; eval(puts '=== hello there'.inspect);
and set the attribute of the payload equal to the value like this:
unsafe_object["foo; eval(puts '=== hello there'.inspect);" ] = struct
Now when you run this code you will get an error, but before that error any code you put in that eval() will be executed:
# => "=== hello there"
This behavior isn’t inherently unsafe, after all we had to manually build our exploit string and manually instantiate our class. The problem only comes when we put all of these things together.

The Exploit

We know we have a class that runs arbitrary code:
unsafe_object = ActionDispatch::Routing::RouteSet::NamedRouteCollection.new
struct        = OpenStruct.new(defaults: {})
unsafe_object["foo; eval(puts '=== hello there'.inspect);"] = struct
# => "=== hello there"
And we know we can build objects like this using YAML:
--- !ruby/hash:ActionDispatch::Routing::RouteSet::NamedRouteCollection
 'foo; eval(eval(puts '=== hello there'.inspect);': !ruby/object:OpenStruct
   table:
    :defaults: {}
Even with both of these elements our Rails app is still safe, unless users are allowed to send the application arbitrary YAML that gets loaded. As you’ve likely guessed, there was a bug that allowed a malicious user to use an XML request to inject YAML into a Rails app. When you put the three elements together, you have a system that can be completely taken over by a malicious user. If someone can run arbitrary code on your server, you don’t own that server anymore, they do.

Hindsite 20/20

The holes in Rails XML and JSON parsers for different vulnerable versions have been fixed, and some have asked why they weren’t detected and patched earlier. The simple answer is: security is hard. These issues are only obvious in retrospect. Rails and Ruby aren’t any less secure than other frameworks and languages. Security vulnerabilities are bugs at their core, and very difficult to detect. There is almost guaranteed to be insecure software on your laptop/phone/server/garage-door-opener somewhere – it just hasn’t been discovered yet. Hopefully you’ve gotten a taste for how difficult it can be to spot these vulnerabilities. We can arm ourselves with knowledge and a healthy distrust for user submitted information.

Knowledge is Power

The more people who understand how YAML can be used against a system, the easier it will be to detect a security hole before it’s put in production. When vulnerabilities are discovered, it is important that developers around that software understand the root causes and help spread knowledge to others. I’m not a security researcher or specialist, but just a guy who’s been writing Rails code for a few years. Until I started researching this attack, I didn’t know why, but now I’ll never forget.

Never Trust Your Users

The lesson isn’t that YAML is evil, it’s still as awesome as it ever was. The lesson is that you should never trust your user’s input. If an attacker can’t touch your code or use it in unexpected ways, they can’t exploit it. This is hard to do in practice, but a little extra attention can go a long way.

Stay Up to Date

Subscribe to mailing lists or groups that post security announcements for the major pieces of software you use such as the the Rails Security group. When an announcement goes out, upgrade immediately even if the threat is small. Hopefully you’ve seen how one unexpected use of a software can lead to another. The company I work for, Heroku, considered the threat from this issue so severe that we notified the people who are running vulnerable code. Even with all the knowledge in the world, it doesn’t do you any good if you continue to run software with known exploits.

Recap

You might not be a security researcher, but hopefully you’ve learned a thing or two not only about this YAML attack, but also the nature of a security exploit. You’ve learned to distrust user input, and you’ve learned that knowledge is power. So go share your knowledge by sending this article out to another developer, they might appreciate it.

Frequently Asked Questions (FAQs) about Rails YAML Vulnerability

What is Rails YAML vulnerability and why is it a concern?

Rails YAML vulnerability is a security flaw in the Ruby on Rails web application framework. It allows an attacker to execute arbitrary code on the server running a Rails application. This vulnerability is a concern because it can lead to unauthorized access, data theft, or even a complete system takeover. It’s crucial for developers and system administrators to understand and mitigate this vulnerability to protect their applications and systems.

How does the Rails YAML vulnerability work?

The Rails YAML vulnerability exploits the way Ruby on Rails parses parameters. An attacker can craft a malicious YAML document that, when parsed by the Rails application, results in the execution of arbitrary code. This is possible because Rails uses the YAML.load method, which can deserialize arbitrary Ruby objects, potentially leading to remote code execution.

How can I protect my Rails application from the YAML vulnerability?

To protect your Rails application from the YAML vulnerability, you should avoid using the YAML.load method to parse parameters. Instead, use safe methods like YAML.safe_load, which only deserializes simple YAML documents and doesn’t allow arbitrary Ruby object deserialization. Additionally, always keep your Rails framework updated to the latest version, as security patches are regularly released to fix known vulnerabilities.

What is the difference between YAML.load and YAML.safe_load?

YAML.load is a method in Ruby that parses a YAML document and deserializes it into a Ruby object. However, it can deserialize any Ruby object, which can lead to security vulnerabilities. On the other hand, YAML.safe_load only allows deserialization of simple YAML documents and doesn’t allow arbitrary Ruby object deserialization, making it a safer choice.

What is remote code execution (RCE)?

Remote Code Execution (RCE) is a type of security vulnerability that allows an attacker to run arbitrary code on a victim’s system. This can lead to unauthorized access, data theft, or even a complete system takeover. RCE vulnerabilities are considered critical and should be addressed immediately.

How can I detect if my Rails application is vulnerable to the YAML vulnerability?

You can use security scanning tools to detect if your Rails application is vulnerable to the YAML vulnerability. These tools can scan your codebase for known vulnerabilities and provide detailed reports. Additionally, always keep an eye on security advisories and updates from the Rails community.

What is a YAML document?

YAML, which stands for “YAML Ain’t Markup Language”, is a human-readable data serialization format. It’s often used for configuration files and in applications where data is being stored or transmitted. A YAML document is a file that contains data formatted according to the YAML specification.

What is the impact of the Rails YAML vulnerability?

The impact of the Rails YAML vulnerability can be severe. It can lead to unauthorized access, data theft, or even a complete system takeover. An attacker can execute arbitrary code on the server running a vulnerable Rails application, potentially leading to serious consequences.

Are there any real-world examples of the Rails YAML vulnerability being exploited?

While there are no widely reported real-world examples of the Rails YAML vulnerability being exploited, it’s a theoretical possibility. The vulnerability is well-documented and tools exist to exploit it. Therefore, it’s crucial to take preventative measures to protect your Rails applications.

What are some best practices for secure coding in Rails?

Some best practices for secure coding in Rails include keeping the Rails framework and all dependencies up-to-date, using secure methods for parsing parameters (like YAML.safe_load instead of YAML.load), validating and sanitizing user input, using strong encryption for sensitive data, and regularly auditing your codebase for security vulnerabilities.

Richard SchneemanRichard Schneeman
View Author

Ruby developer for Heroku. Climbs rocks in Austin & teaches Rails classes at the University of Texas. You can see more of Richard's work at http://schneems.com/

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week