How do we really know if a path is safe when serving files?

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

In wai-app-static we also filter out files beginning with just a single '.'
(assuming they are supposed to be hidden files).
The WAI protocol already breaks up a request into pieces. When you switch
over to WAI we would love to combine forces with you on wai-app-static. We
now have caching headers, embedded file support, and a test suite. We have
already stolen a few things from the Happstack file serving code.
Greg Weber
On Mon, Jun 27, 2011 at 2:53 PM, Jeremy Shaw
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
_______________________________________________ web-devel mailing list web-devel@haskell.org http://www.haskell.org/mailman/listinfo/web-devel
participants (2)
-
Greg Weber
-
Jeremy Shaw