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.