Introduction
I’ve recently been writing a Go backend that accesses many local files. One of the most important issues to guard against is path traversal. Go 1.24 introduced the os.Root API, which provides a concise and elegant way to avoid this security problem.
The problem
When searching for files in a directory, an attacker can simply use ../ to escape the intended directory and read arbitrary files on the server.
baseDirectory := "/app/uploads"filename := "../../../etc/passwd" // BAD PATH
f, err := os.Open(filepath.Join(baseDirectory, filename))Traditional way of defenses
Traditional defenses typically validate and sanitize paths to avoid malicious input:
- Check for
.. - Use
filepath.Clean - Validate prefix
- Compare absolute paths
cleaned := filepath.Clean("/app/uploads/" + filename)
if !strings.HasPrefix(cleaned, "/app/uploads/") { return errors.New("invalid path")}But incorrect handling can still be insecure. For example, the following check misses a trailing slash (it should be /app/uploads/), which creates a vulnerability because /app/uploads becomes a string prefix of /app/uploads_secret but is not its path prefix:
filename := "../uploads_secret/flag.txt"cleaned := filepath.Clean("/app/uploads/" + filename)
if !strings.HasPrefix(cleaned, "/app/uploads") { return errors.New("invalid path")}
// filename = /app/uploads_secret/flag.txtBesides error-prone string validations, other issues like symbolic links, TOCTOU (time-of-check to time-of-use) race conditions, and platform differences make manually composing file paths very troublesome.
The os.Root API solution
Regardless of what path the user provides, access is limited to files under the specified root
root, err := os.OpenRoot("/app/uploads")
if err != nil { log.Fatal(err)}
defer root.Close()
f, err := root.Open("avatar/user-1.png")No extra packages like: safeopen, nor is strict string validation needed. Simply define a Root path; any access outside of it is illegal.
Conclusion
Consider whether all uses of os.Open are safe, and replace them with os.Root to ensure operations cannot access files outside the directory.
// Might escape baseDirectory.f, err := os.Open(filepath.Join(baseDirectory, filename))
// Will always be inside baseDirectoryf, err := os.OpenInRoot(baseDirectory, filename)For single and multiple operations you can use os.OpenInRoot or os.Root. The difference is that OpenInRoot is just OpenRoot + r.Open + defer r.Close().
// Single operationf, err := os.OpenInRoot(baseDirectory, filename)
// Multiple operationsroot, err := os.OpenRoot(baseDirectory)