flawopen.com/Path Traversal/Go
You give a clerk a storage box key and ask for item 'receipts/../keys'. Without checking, the clerk steps outside the receipts box and hands you the building master keys.
Go's filepath.Join(uploadDir, filename) calls filepath.Clean on the result. If filename is ../../etc/passwd, filepath.Join produces /etc/passwd if uploadDir is not deeply nested, escaping boundaries.
In 2024, CVE-2024-21626 (Leaky Vessels) in runc demonstrated how container runtimes in Go failed to sanitize file descriptor and directory paths, allowing breakout to the host root filesystem.
CISA Cybersecurity Advisory & MITRE CVE repository.// VULNERABLE: filepath.Join does not prevent directory breakout
package main
import (
"net/http"
"os"
"path/filepath"
)
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
baseDir := "/var/app/public/files"
// Attacker input: "../../etc/shadow"
targetPath := filepath.Join(baseDir, filename)
data, err := os.ReadFile(targetPath)
if err != nil {
http.NotFound(w, r)
return
}
w.Write(data)
}
// HARDENED: Use filepath.Rel to verify target is strictly inside baseDir
package main
import (
"net/http"
"os"
"path/filepath"
"strings"
)
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
baseDir := "/var/app/public/files"
// 1. Resolve absolute paths
absBase, err := filepath.Abs(baseDir)
if err != nil {
http.Error(w, "Server error", http.StatusInternalServerError)
return
}
absTarget := filepath.Join(absBase, filepath.Clean("/"+filename))
// 2. Verify relative containment
rel, err := filepath.Rel(absBase, absTarget)
if err != nil || strings.HasPrefix(rel, "..") || rel == "." {
http.Error(w, "Access denied: Path Traversal detected", http.StatusForbidden)
return
}
data, err := os.ReadFile(absTarget)
if err != nil {
http.NotFound(w, r)
return
}
w.Write(data)
}
filepath.Rel(absBase, absTarget) computes the path difference. If absTarget escapes absBase, the relative path must begin with ... Rejecting strings with the .. prefix guarantees the file is contained.
filepath.Rel is purely lexical. If a symlink in baseDir points to /etc, filepath.Rel passes. Use filepath.EvalSymlinks() when untrusted users can create files.
filepath.Clean resolves dots, but if the input begins with ../../, it preserves those leading dots if the path is relative.
gosec -include=G304 ./...
filepath.Clean()!strings.HasPrefix(rel, "..") using filepath.Rel()filepath.EvalSymlinks() where necessaryhttp.FS or os.DirFS() which enforce chroot-like root boundariesYes. Go 1.16+ io/fs and os.DirFS explicitly reject path requests containing leading slashes or '..' sequences, returning fs.ErrInvalid.