Security vulnerability: Path traversal in `DirectoryHTTPHandler`
stackotter opened this issue · comments
What is a path traversal vulnerability?
A path traversal vulnerability occurs when paths aren't properly normalised by the server, allowing attackers to access any file on the server, not just files within the served directory.
Proof of concept
The following code snippet for creating a simple file server is vulnerable to path traversal (and so is any other code that uses DirectoryHTTPHandler
).
let directoryHandler = DirectoryHTTPHandler(root: URL(fileURLWithPath: "."), serverPath: "/")
let server = HTTPServer(port: 80)
await server.appendRoute("GET *", to: directoryHandler)
try await server.start()
To observe the effect of this vulnerability, run the code snippet above and then run the following command in terminal to see that the server has exposed the contents of your machine's /etc/passwd
file (and all other files for that matter):
curl --path-as-is http://127.0.0.1/../../../../../../../../../../../etc/passwd
Any user that can access the web server can run a similar command on their machine to access any file on the server (within the limits of the privileges that the server is running with).
Mitigation
Option 1 (easy, but isn't as nice as option 2)
Abort any requests to the directory handler that contain ../
in their path. This is how vapor's file server middleware avoids path traversal (the following snippet is taken from vapor source). This only fixes the issue for the built-in directory handler, but the check could be applied to all requests (not just those handled by the directory handler) to fix that.
// protect against relative paths
guard !path.contains("../") else {
return request.eventLoop.makeFailedFuture(Abort(.forbidden))
}
I don't like this solution because it feels like a bit of a quick fix, but another way of looking is that its simplicity makes it very hard to mess up.
Option 2
Normalize request paths in HTTPDecoder
to remove any ..
components. Refer to the RFC specification on removing dot components (https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4).
This solution is a bit more involved, but it results in nicer behaviour in my opinion, and I think it's the approach that most http servers take.
Conclusion
Let me know which solution you prefer. If you would like me to implement the fix, I'd be happy to implement either solution. Personally I prefer the behaviour of the second solution but the simplicity of the first, so it's up to you.
Thank you for your analysis here — amazing.
I do think it would be worth fixing within HTTPDecoder
so that any HTTPHandler
receives the fix. If you are willing to work on a solution then it would be really appreciated 🙏.
If it helps, we may be able to utilise Foundation's URL.standardized. From my short investigation it appears to offer sensible expansion of .
and ..
// leading / is preserved here;
URL(string: "/fish/and/../chips")?.standardized.path
// /fish/chips
// but leading / is lost here (not sure, what is expected)
URL(string: "/fish/../../../../../chips")?.standardized.path
// chips
// ~ is not expanded
URL(string: "~/chips")?.standardized.path
// ~/chips
It may be possible to simply use it within HTTPDecoder
like so;
URL(string: path)?.standardized.path
Hello! Thanks for this! I'm the original author of that handler. 😬
The URL.standardized
method seems to match all of the test cases I could find for examples of this algorithm in my brief look:
func doItProperly(with path: String) -> String? {
URL(string: path)?.standardized.path
}
doItProperly(with: "/a/b/c/./../../g") // "/a/g" -- from RFC
doItProperly(with: "mid/content=5/../6") // "mid/6" -- from RFC
doItProperly(with: "foo/./bar/baz/../qux") // "foo/bar/qux" -- from Wiki
doItProperly(with: "/../a/b/../c/./d.html") // "a/c/d.html" -- can't remember
I believe if the path has too many ..
then it will drop the leading /
, and that is what is happening in your "/fish/../../../../../chips"
. From my (sleepy) interpretation of the RFC that is correct.
Yep, that looks like it should fix the issue 👍 Good idea using Foundation's URL, that's a lot more reliable than implementing a custom algorithm