Unrestricted File Download in Python
Vulnerable Example
The snippet below defines a Flask endpoint that serves files from the /opt/wwwdata/assets/
asset folder depending on the file name passed as asset_name
.
asset_folder = '/opt/wwwdata/assets/'
@app.route('/assets')
def get_asset():
asset_name = request.args.get('asset_name')
if not asset_name:
return 404
return send_file(os.path.join(asset_folder, asset_name))
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 os.path.abspath()
to get the absolute path, and then check that it starts as expected.
asset_folder = '/opt/wwwdata/assets/'
@app.route('/assets')
def get_asset():
asset_name = request.args.get('asset_name')
if not asset_name:
return 404
absolute_file_path = os.path.abspath(os.path.join(asset_folder, asset_name))
if not absolute_file_path.startswith(asset_folder):
return 403
return send_file(absolute_file_path)
Alternatively, to sanitize the path, it is possible to use Flask’s component werkzeug.secure_filename()
to properly sanitize any user-provided input.
from werkzeug.utils import secure_filename
asset_folder = '/opt/wwwdata/assets/'
@app.route('/assets')
def get_asset():
asset_name = request.args.get('asset_name')
if not asset_name:
return 404
asset_name = secure_filename(asset_name)
return send_file(os.path.join(asset_folder, asset_name))
The previous attack would result in the /opt/wwwdata/assets/etc_passwd
file being read.
FastAPI
In FastAPI, the StaticFiles
function is used to serve files from a directory securely. When you mount the StaticFiles
instance to the assets path, it takes care of handling the files in that directory, preventing path traversal attacks by ensuring that only files within the specified directory can be served.
app = FastAPI()
app.mount("/assets", StaticFiles(directory="/opt/wwwdata/assets"), name="assets")