flawopen.com/Path Traversal/Go

Path Traversal in Go

High Severity CWE-22
ELI5

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.

Key terms on this page
filepath.Clean
Lexically cleans a path by resolving relative dots, but does not prevent the path from resolving to parent directories (../).
filepath.Rel
Computes the relative path between basepath and targpath. If the result starts with '..', targpath is outside basepath.

What's happening

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.

Real-world impact

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 vs. fixed

VULNERABLE
// 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)
}
FIXED
// 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)
}

Why the fix works

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.

Gotchas

Symlinks bypass filepath.Rel

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.

Common misconceptions

"filepath.Clean prevents directory traversal"

filepath.Clean resolves dots, but if the input begins with ../../, it preserves those leading dots if the path is relative.

How to check if you're affected

gosec -include=G304 ./...

Prevention checklist

FAQ

Does os.DirFS prevent path traversal?

Yes. Go 1.16+ io/fs and os.DirFS explicitly reject path requests containing leading slashes or '..' sequences, returning fs.ErrInvalid.

References