[web-devel] How do we really know if a path is safe when serving files?

Jeremy Shaw jeremy at n-heptane.com
Mon Jun 27 23:53:55 CEST 2011


Hello,

Most frameworks provide some sort of mechanism for serving files from
a directory. Generally some subset of the url is mapped directly to
files on the disk. But, with this comes the risk that users will be
able to access files they should not be able to. For example,
something like:

http://localhost:8000/../../../etc/passwd

The tricky question is, how do we determine what paths are safe and
what paths are not.

I think the first step is to break the url into path segments and url
decode each segment. So something a bit like this:

pathElements :: String -> [String]
pathElements = map urlDecode .  splitOnChar '/'

note that we want to split on '/' before we decode. That is because
the url might contain an embedded %2f, which is the '/' character. It
is encoded as %2f precisely because it is *not* supposed to treated as
a path separator.

Now that we have a list of path segments we want to check that those
path segments can actually be mapped to a valid and safe path on the
disk. So, we need to filter out any path segments that contain
embedded path separators (like / or \ depending on the platform).
Those characters can appear in a valid url. But they can never appear
in a valid path segment (they can only be used as path separators).

We also want to reject any path that contains embedded "..". Or do we?
In theory, we could canonicalize  path, "foo/../bar" to just "bar"?
Certainly *after* canonicalization, we want to reject any path that
contains a ".." segment. Under windows we have some extra concerns.
For example, there are special names like LPT1, COM1, etc. Also, we do
not want to allow someone to specify a drive name like C:\foo.

But, how do we know if we have gotten everything right ?

An alternative method would be to use
System.Directory.canonicalizePath on the root directory we are serving
from and the requested file. Then we check that the canonicalized root
directory is the prefix of the canonicalized requested path:

isChild :: FilePath -> FilePath -> IO Bool
isChild root requested =
  do root' <- canonicalizePath root
       requested' <- canonicalizePath requested
       return (root' `isPrefixOf` requested')

(Needs to handle exceptions when the root or requested path does not
actually exist).

One issue with that solution is that it disallows the use of symlinks
which point to directories outside of the root. That can be viewed as
either a feature or a bug. It is also not clear that it is actually
free of any security issues.

So, this is what I have so far. First I use a function like the above
'pathElements' to split the url into path segments.

Then I use this function to test that it is a safe/valid path:

isSafePath :: [FilePath] -> Bool
isSafePath [] = True
isSafePath (s:ss) =
     isValid s
  && (all (not . isPathSeparator) s)
  && not (hasDrive s)
  && not (isParent s)
  && isSafePath ss

-- note: could be different on other OSs
isParent :: FilePath -> Bool
isParent ".." = True
isParent _    = False

Something like this:

let pathEls = pathElements requestPath
in if (isSafePath pathEls)
    then let fp = joinPath (rootPath : pathEls) in ....
    else fail "unsafe path"

Does anyone see any issues with this? Security bugs or otherwise?

Thanks!
- jeremy



More information about the web-devel mailing list