Directory traversal is another injection-style attack, wherein a malicious user tricks filesystem code into reading and/or writing files that the Web server shouldn’t have access to. An example might be a view that reads files from the disk without carefully sanitizing the file name:
def dump_file(request):
filename = request.GET[“filename”]
filename = os.path.join(BASE_PATH, filename)
content = open(filename).read()
# …
Though it looks like that view restricts file access to files beneath BASE_PATH (by using os.path.join), if the attacker passes in a filename containing .. (that’s two periods, a shorthand for “the parent directory”), she can access files “above” BASE_PATH. It’s only a matter of time before she can discover the correct number of dots to successfully access, say, ../../../../../etc/passwd. Anything that reads files without proper escaping is vulnerable to this problem. Views that write files are just as vulnerable, but the consequences are doubly dire.
Another permutation of this problem lies in code that dynamically loads modules based on the URL or other request information. A well-publicized example came from the world of Ruby on Rails. Prior to mid-2006, Rails used URLs like http://example.com/person/poke/1 directly to load modules and call methods. The result was that a carefully constructed URL could automatically load arbitrary code, including a database reset script!
The Solution – If your code ever needs to read or write files based on user input, you need to sanitize the requested path very carefully to ensure that an attacker isn’t able to escape from the base directory you’re restricting access to.
Note – Needless to say, you should never write code that can read from any area of the disk!
A good example of how to do this escaping lies in Django’s built-in static content-serving view (in
django.views.static). Here’s the relevant code:
import os
import posixpath
# …
path = posixpath.normpath(urllib.unquote(path))
newpath = ”
for part in path.split(‘/’):
if not part:
# strip empty path components
continue
drive, part = os.path.splitdrive(part)
head, part = os.path.split(part)
if part in (os.curdir, os.pardir):
# strip ‘.’ and ‘..’ in path
continue
newpath = os.path.join(newpath, part).replace(‘\\’, ‘/’)
Django doesn’t read files (unless you use the static.serve function, but that’s protected with the code just shown), so this vulnerability doesn’t affect the core code much.
In addition, the use of the URLconf abstraction means that Django will never load code you’ve not explicitly told it to load. There’s no way to create a URL that causes Django to load something not mentioned in a URLconf.