Claudiu
Claudiu

Reputation: 229521

Secure user-provided filename

Part of my app requires the client to request files. Now, a well-behaved client will only request files that are safe to give, but I don't want a user to go about supplying "../../../creditCardInfo.xls", instead. What's the best practice for/simplest way to secure a filename to make sure that no files are served that would be higher than a certain point in the directory hierarchy? First instinct is to disallow filenames with .. in them but that seems... incomplete and unsatisfactory.

The current questions about filename safety on SO focus on making a writable/readable filename, not ensuring that files that shouldn't be accessed are accessed.

Upvotes: 2

Views: 1578

Answers (4)

Jason P
Jason P

Reputation: 27022

Here's the approach I use, which I think has the benefits of giving easy control over access to the files, and preventing path manipulation.

When the user uploads the file:

  1. Read the filename.
  2. Generate a random alphanumeric token.
  3. Save the file in a non-web accessible directory with a filename of that token.
  4. Record the token and original filename in a database (along with who uploaded it, or some way of indicating who has permission to it).

To get the file:

  1. The user requests the file via the token, not a filepath (mysite.com/download/587and83j21h1)
  2. Use white-list validation on the token to ensure that it is alphanumeric.
  3. Check the user's permission to the requested file.
  4. Write the file to the response stream and set the filename equal to the original filename.

Upvotes: 0

Sylvain Leroux
Sylvain Leroux

Reputation: 52040

I think you are looking for a way to find the canonical(*) path of a file. That is with .., . and symbolic links removed. That is the role of os.path.realpath

>>> os.path.realpath(".")
'/home/sylvain'
>>> os.path.realpath("..")
'/home'
>>> os.path.realpath("./Documents/../..")
'/home'

realpath will follow symlink and "reduce" the path as well:

sylvain@daal:~$ ln -s /etc/password z
sylvain@daal:~$ python 
Python 2.6.6 (r266:84292, Dec 26 2010, 22:31:48) 
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os.path
>>> os.path.realpath("z")
'/etc/password'
>>> os.path.realpath("z/..")
'/etc'
>>> os.path.realpath("./Documents/../z/..")
'/etc'

... whereas normpath is easily abused:

>>> os.path.normpath("./Documents/../z/..")
'.'

Once you have the canonical name, you could easily check if the user should have access to the requested file. Say by comparing to a white-list.


(*) A file may have different path but only one canonical path.
See http://www.xyzws.com/Javafaq/what-is-the-difference-between-absolute-relative-and-canonical-path-of-file-or-directory/60 for more info.

Upvotes: 0

brabster
brabster

Reputation: 43590

If you're running in a UNIX variant, you might want a chroot jail to prevent access to the system outside your application.

This approach would avoid you having to write your own code to deal with the problem and let you handle it with infrastructure setup. It might not be appropriate if you need to restrict access to some area within the application as it changes what the process thinks is the system root.

Upvotes: 1

Claudiu
Claudiu

Reputation: 229521

This seems like it would work, provided that open uses the same mechanism to resolve paths as os.path.abspath. Are there any flaws to this approach?

import os

def is_safe(filename):
    here = os.path.abspath(".")
    there = os.path.abspath(filename)
    return there.startswith(here)

>>> is_safe("foo.txt")
True
>>> is_safe("foo/bar/baz")
True
>>> is_safe("../../goodies")
False
>>> is_safe("/hax")
False

Upvotes: 1

Related Questions