Link Search Menu Expand Document

Unrestricted File Download in Ruby

Vulnerable Example

The snippet below defines a web endpoint with Ruby on Rails that serves files from the /opt/wwwdata/assets/ asset folder depending on the file name passed as asset_name.

class AssetController < ApplicationController
  def download
   begin
     path = "/opt/wwwdata/assets/" + params[:asset_name].to_s
     send_file path, disposition: "attachment"
   rescue
     redirect_to "/home"
   end
  end
end

Since asset_name is controlled by the user, it is possible to conduct a path traversal attack and escape the intended directory, for example, by using ../../../../../../etc/passwd as the file asset name.

This results in an unexpected file disclosure since the absolute path /opt/wwwdata/assets/../../../../../etc/passwd is canonicalized by the operating system as /etc/passwd.

Prevention

To validate that a path does not point to an unintended location, use File.expand_path() to get the absolute path, and then check that it starts as expected.

class AssetController < ApplicationController
  def download
   begin
     path = File.expand_path("/opt/wwwdata/assets/" + params[:asset_name].to_s)
     if path.start_with?("/opt/wwwdata/assets/")
       send_file path, disposition: "attachment"
     else
        head 403
     end
   rescue
     redirect_to "/home"
   end
  end
end

Alternatively, extract the file name from the last part of the path and use that to build the full path, as shown in the following snippet.

class AssetController < ApplicationController
  def download
   begin
     path = '/opt/wwwdata/assets/' + File.basename(params[:asset_name].to_s)
     send_file path, disposition: "attachment"
   rescue
     redirect_to "/home"
   end
  end
end

Yet another possibility is to sanitize the user-provided input. This is an example of a pretty aggressive allow list approach that sanitizes any dangerous character.

def sanitize(filename)
  # Remove any characters that aren't 0-9, A-Z, a-z, dash, underscore, or dot.
  filename.gsub(/[^0-9A-Z_.-]/i, '_')
end

class AssetController < ApplicationController
  def download
   begin
     path = "/opt/wwwdata/assets/" + sanitize(params[:asset_name].to_s)
     send_file path, disposition: "attachment"
   rescue
     redirect_to "/home"
   end
  end
end

The previous attack would result in the /opt/wwwdata/assets/.._.._.._.._.._.._etc_passwd file being read.

References

Owasp - Path Traversal