While working on a Golang project that involves serving static files, I realized that my code was vulnerable to path traversal attacks. Here is a snippet of the code:
cleanPath := path.Clean(r.URL.Path)
publicPath := path.Join("public", cleanPath)
file, err := os.Open(publicPath)
...
http.ServeContent(w, r, file.Name(), time.Time{}, file)
And here is the documentation for path.Clean
(docs):
Clean returns the shortest path name equivalent to path by purely lexical processing. It applies the following rules iteratively until no further processing can be done:
- Replace multiple slashes with a single slash.
- Eliminate each . path name element (the current directory).
- Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it.
- Eliminate .. elements that begin a rooted path: that is, replace ”/..” by ”/” at the beginning of a path.
The returned path ends in a slash only if it is the root ”/“. If the result of this process is an empty string, Clean returns the string ”.“.
If that doesn’t make sense to you, here are some examples:
path.Clean("a/c") // "a/c"
path.Clean("a//c") // "a/c"
path.Clean("a/c/.") // "a/c"
path.Clean("a/c/b/..") // "a/c"
path.Clean("/../a/c") // "/a/c"
path.Clean("/../a/b/../././/c") // "/a/c"
path.Clean("") // "."
path.Clean("/../a/c")
returns /a/c
, that’s what we want, right?
The problem
There is a very important note in the documentation of the path
package (docs):
The path package should only be used for paths separated by forward slashes, such as the paths in URLs. This package does not deal with Windows paths with drive letters or backslashes; to manipulate operating system paths, use the path/filepath package.
What this means is that path.Clean
will not clean /..\..\foo
. Fortunately for me, only Windows uses \
as path separator, meaning the code is only vulnerable on Windows, and I don’t use Windows for internet-facing services.
Solutions
There are many ways to prevent path traversal attacks in Go on all platforms. Here are a few, in no particular order:
filepath.Clean (docs)
Unlike path.Clean
, filepath.Clean
uses the operating system’s path seperator(s) instead of just /
.
⚠️ The input path must start with a slash, or it will not be cleaned:
filepath.Clean("..\\foo") // "..\foo"
filepath.Clean("/..\\foo") // "\foo"
HTTP request paths (r.URL.Path
) always start with a slash.
filepath.Join (docs)
filepath.Join
cleans the result using filepath.Clean
before returning, so it can be used to prepend a slash and clean the path at the same time:
filepath.Join("/", "..\\..\\foo") // "\foo"
http.Dir (docs)
http.Dir
restricts file system access to a specific directory tree.
filepath.IsLocal (docs)
Please refer to the docs for an explanation.
Here are some examples, since the docs don’t have any:
filepath.IsLocal("/foo") // false: is an absolute path
filepath.IsLocal("../foo") // false: is not within the subtree rooted at the directory in which path is evaluated
filepath.IsLocal("") // false: is empty
filepath.IsLocal("foo/bar") // true
Check if a path element is equal to ..
This technique is used by http.ServeFile
(code).
Check if the path contains ..
I don’t recommend using this technique because file names can contain ..
.
Open-source projects
After fixing my code, I searched for open-source Go projects with path traversal vulnerabilities. Here is what I found:
viws
The first vulnerable project I found was viws, a file server. It attempts to prevent path traversal attacks by rejecting requests with a path containing ..
(code):
if strings.Contains(r.URL.Path, "..") {
httperror.BadRequest(w, errors.New("path with dots are not allowed"))
}
An attentive reader might have noticed the mistake: there is no return
statement! There are no other defenses against path traversal attacks (including path.Clean
) either, so it’s vulnerable on Linux too!
Client output:
C:\Users\Rowin>curl --path-as-is http://127.0.0.1:1080/../secret.txt
path with dots are not allowed
Ts0!28woA$Q1$!#g
Server output:
C:\Users\Rowin\viws>go run cmd/viws/viws.go -directory public
2023-03-02T19:54:25+01:00 INFO Serving file dir="public"
2023-03-02T19:54:25+01:00 WARNING Listening on :9090 without TLS server="prometheus"
2023-03-02T19:54:25+01:00 WARNING Listening on :1080 without TLS server="http"
2023-03-02T19:55:36+01:00 WARNING HTTP/400: path with dots are not allowed
2023/03/02 19:55:36 http: superfluous response.WriteHeader call from github.com/ViBiOh/httputils/v4/pkg/prometheus.(*observableResponseWriter).WriteHeader (observable_writer.go:60)
I added the missing return
statement and opened a pull request, and it was merged shortly after.
srv
The second vulnerable project I found was srv, another file server. The project itself has no defenses against path traversal attacks, but because Go’s ServeMux cleans the path using path.Clean
(code), it is only vulnerable on Windows.
Client output:
C:\Users\Rowin>curl --path-as-is http://127.0.0.1:8000/..\secret.txt
Ts0!28woA$Q1$!#g
Server output:
C:\Users\Rowin\srv>go run main.go public
2023/03/02 21:38:33 Serving public over HTTP on 127.0.0.1:8000
2023/03/02 21:38:38 127.0.0.1:63564 [curl/7.83.1]: GET HTTP/1.1 127.0.0.1:8000/..\secret.txt
Conclusion
While I only found two vulnerable projects, I am sure there is more vulnerable code out here. Hopefully this blog post helps people avoid path traversal vulnerabilities in Go.