What is os.Root (Go 1.24), and how does it prevent path-traversal attacks compared to filepath.Join plus filepath.Clean?
os.OpenRoot(dir) returns an *os.Root. All of its operations (Open, Create, OpenFile, Mkdir, Remove, Stat, Lstat, plus ReadFile, WriteFile, MkdirAll, Rename, RemoveAll and others added in Go 1.25) are guaranteed to stay inside that directory. It rejects .. escapes and absolute paths, and it follows symlinks only when their target stays inside the root. On Unix it resolves paths one component at a time with openat relative to the directory handle, which also closes the check-then-use (TOCTOU) race.
// The naive approach - what does this print?
fmt.Println(filepath.Join("/srv/uploads", "../../etc/passwd")) // /etc/passwd
// Clean normalizes but does not confine; a symlink uploads/evil -> /etc
// also defeats any strings.HasPrefix(cleaned, base) check.
root, err := os.OpenRoot("/srv/uploads")
if err != nil {
return err
}
defer root.Close()
f, err := root.Open(userName) // "../../etc/passwd" -> error: path escapes from parent
if err != nil {
return err
}
defer f.Close()
// One-shot helper and fs.FS view:
f2, err := os.OpenInRoot("/srv/uploads", userName)
fsys := root.FS() // use with fs.WalkDir, http.FileServerFS, etc.
filepath.Join plus Clean is purely lexical. It knows nothing about symlinks, and a prefix check is racy because a symlink can be swapped in after the check. filepath.IsLocal (Go 1.20) and filepath.Localize (1.23) help validate names, but they are still lexical. Caveats: os.Root does not protect against Linux bind mounts or special files in /proc, and on js/wasm and plan9 it can only do lexical checks. What the interviewer is looking for: lexical checks are not enough against symlinks, and you should use a descriptor-relative API for any path a user controls.
More on More Standard Library Essentials
- Q541How do you build a TCP server with net.Listen? Handle per-connection goroutines, read/write deadlines, and graceful shutdown of the listener.
- Q542Explain io/fs and fs.FS. How do you walk a directory with filepath.WalkDir and test file-system code with testing/fstest.MapFS?
- Q544How do you parse CLI flags with the flag package? How do subcommands work, and why is flag.Parse in init() a bug?
- Q545Compare strconv with fmt for number/string conversion. What do strconv.ParseInt's base and bitSize arguments do, and how do you check for range errors?
- Q546When would you use encoding/binary, encoding/gob or protobuf for serialization? What are the byte-order and compatibility pitfalls?
- Q547How do signals work in Go (os/signal)? Why must the channel passed to signal.Notify be buffered?