Avoid path traversal by utilizing Go os.Root

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.txt

Besides 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 baseDirectory
f, 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 operation
f, err := os.OpenInRoot(baseDirectory, filename)
// Multiple operations
root, err := os.OpenRoot(baseDirectory)

Further reading