Preventing Directory Traversal in PHP but allowing paths

Secure coding in php
Márcio Rossi
Posts: 21
Joined: 09 Dec 2014 12:21

Preventing Directory Traversal in PHP but allowing paths

Post by Márcio Rossi » 10 Dec 2014 10:58

I have a base path /whatever/foo/

and
$_GET['path'] should be relative to it.

However how do I accomplish this (reading the directory), without allowing directory traversal?

eg.

/\.\.|\.\./


Will not filter properly.

Alexandre Medeiros
Posts: 27
Joined: 02 Dec 2014 17:38

Re: Preventing Directory Traversal in PHP but allowing paths

Post by Alexandre Medeiros » 10 Dec 2014 10:58

Well, one option would be to compare the real paths:

$basepath = '/foo/bar/baz/';
$realBase = realpath($basepath);

$userpath = $basepath . $_GET['path'];
$realUserPath = realpath($userpath);

if ($realUserPath === false || strpos($realUserPath, $realBase) !== 0) {
//Directory Traversal!
} else {
//Good path!
}


Basically, realpath() will resolve the provided path to an actual hard physical path (resolving symlinks, .., ., /, //, etc)... So if the real user path does not start with the real base path, it is trying to do a traversal. Note that the output of realpath will not have any "virtual directories" such as . or .....

A Leopold
Posts: 11
Joined: 09 Dec 2014 12:44

Re: Preventing Directory Traversal in PHP but allowing paths

Post by A Leopold » 10 Dec 2014 10:58

@ircmaxell the answer wasn't fully correct. I've seen that solution in several snippets but it has a bug which is related to the output of realpath(). The function realpath() removes the trailing directory separator, so imagine two contiguos directories such as:

/foo/bar/baz/

/foo/bar/baz_baz/

As realpath() would remove the last directory separator you method will return "good path" if $_GET['path'] was equal to "../baz_baz" as it would be something like

strpos("/foo/bar/baz_baz", "/foo/bar/baz")


Maybe:

$basepath = '/foo/bar/baz/';
$realBase = realpath($basepath);

$userpath = $basepath . $_GET['path'];
$realUserPath = realpath($userpath);

if ($realUserPath === false || strcmp($realUserPath, $realBase) !== 0 || strpos($realUserPath, $realBase . DIRECTORY_SEPARATOR) !== 0) {
//Directory Traversal!
} else {
//Good path!
}

Júlio César Mendes
Posts: 16
Joined: 09 Dec 2014 12:17

Re: Preventing Directory Traversal in PHP but allowing paths

Post by Júlio César Mendes » 10 Dec 2014 10:58

It is not sufficient to check for patterns like ../ or the likes. Take "../" for instance which URI encodes to "%2e%2e%2f". If your pattern check happens before a decode, you would miss this traversal attempt. There are some other tricks hackers can do to circumvent a pattern checker especially when using encoded strings.

I've had the most success stopping these by canonicalizing any path string to its absolute path using something like realpath() as ircmaxwell suggests. Only then do I begin checking for traversal attacks by matching them against a base path I've predefined.

Ethan Morton
Posts: 28
Joined: 09 Dec 2014 12:21

Re: Preventing Directory Traversal in PHP but allowing paths

Post by Ethan Morton » 10 Dec 2014 10:58

I assume you mean without allowing users to traverse the directory yes?

If you are trying to stop your own PHP from traversing the directory you should just make the php work properly in the first place.

What you need to stop users is a modified .htaccess file...

Options -Indexes


(This all assumes you are talking about users)

Joaquín Molina
Posts: 26
Joined: 09 Dec 2014 12:20

Re: Preventing Directory Traversal in PHP but allowing paths

Post by Joaquín Molina » 10 Dec 2014 10:58

You may be tempted to try and use regex to remove all ../s but there are some nice functions built into PHP that will do a much better job:

$page = basename(realpath($_GET));

basename - strips out all directory information from the path e.g. ../pages/about.php would become about.php

realpath - returns a full path to the file e.g. about.php would become /home/www/pages/about.php, but only if the file exists.

Combined they return just the files name but only if the file exists.

BUTTON_POST_REPLY