6.1 KiB
Filesystem Security Rules
Filesystem vulnerabilities can lead to unauthorized file access, data leakage, and denial-of-service attacks.
Rules:
- User-controlled file paths MUST be confined to an allowed root.
os.RootSHOULD be used for scoped file access (Go 1.24+).- Zip extraction MUST check for ZipSlip path traversal.
- Temporary files MUST use
os.CreateTemp— NEVER predictable names. - File permissions MUST be restrictive (0600 for secrets, 0750 for directories).
Directory Traversal — High
Paths like ../../etc/passwd access files outside intended directory.
Bad:
filepath := filepath.Join("/var/www", filename) // DON'T
http.ServeFile(w, r, filepath)
Good (Go 1.24+) — use os.Root for safe, scoped directory access:
root, err := os.OpenRoot("/var/www")
if err != nil { return err }
defer root.Close()
f, err := root.Open(filename) // cannot escape root directory
os.Root prevents ordinary path traversal at the OS level. All operations (Open, Create, Stat, OpenFile, etc.) are confined to the root directory, and symlinks that resolve outside the root are rejected. It is not a full sandbox: it does not by itself block bind mounts, special device files, or all /proc-style filesystem behavior. For archive extraction and uploads, still reject special files and choose a root without attacker-controlled mounts.
Good (pre-Go 1.24 fallback):
func safeJoin(baseDir, userPath string) (string, error) {
if userPath == "" || filepath.IsAbs(userPath) || !filepath.IsLocal(userPath) {
return "", errors.New("invalid relative path")
}
full := filepath.Join(baseDir, userPath)
rel, err := filepath.Rel(baseDir, full)
if err != nil {
return "", fmt.Errorf("checking path: %w", err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", errors.New("path escapes base directory")
}
return full, nil
}
This lexical fallback is not a full symlink-resistant substitute for os.Root.
Bad:
fullPath := filepath.Join(baseDir, filename)
if !strings.HasPrefix(filepath.Clean(fullPath), filepath.Clean(baseDir)) {
return errors.New("access denied")
}
Zip Archive Path Traversal — High
Malicious zip files can escape extraction directory.
Bad:
for _, file := range reader.File {
path := filepath.Join(dest, file.Name) // DON'T: No validation
file.Create(path)
}
Good (Go 1.24+) — use os.Root to scope extraction:
root, err := os.OpenRoot(dest)
if err != nil { return err }
defer root.Close()
for _, file := range reader.File {
f, err := root.OpenFile(file.Name, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { return err } // rejects paths escaping root
// ... copy contents ...
f.Close()
}
Good (pre-Go 1.24 fallback):
for _, file := range reader.File {
if !filepath.IsLocal(file.Name) {
return fmt.Errorf("unsafe archive path: %q", file.Name)
}
targetPath, err := safeJoin(dest, file.Name)
if err != nil {
return err
}
// create parent directories, then write targetPath
_ = targetPath
}
Decompression Bomb — Medium
Tiny compressed files can expand to GBs.
Bad:
gr, _ := gzip.NewReader(f)
out, _ := os.Create(dst)
io.Copy(out, gr) // DON'T: No size limits
Good:
const maxDecompressedSize = 100 * 1024 * 1024 // 100MB limit
var errDecompressedSizeLimitExceeded = errors.New("decompressed size limit exceeded")
type limitedReader struct {
r io.Reader
read int64
}
func (l *limitedReader) Read(p []byte) (int, error) {
if l.read >= maxDecompressedSize {
// Return a sentinel error — io.EOF would be treated as success by io.Copy
return 0, errDecompressedSizeLimitExceeded
}
n, err := l.r.Read(p)
l.read += int64(n)
return n, err
}
lr := &limitedReader{r: gr}
if _, err := io.Copy(out, lr); err != nil {
return fmt.Errorf("decompressing: %w", err)
}
Insecure Temporary File Creation — Medium
Creating temp files without proper permissions.
Bad:
f, _ := os.Create("/tmp/myapp.temp") // DON'T: Predictable name
f.WriteString(data)
Good:
f, err := os.CreateTemp("", "myapp.*")
defer os.Remove(f.Name())
f.Chmod(0600) // Restrictive permissions
Insecure File Permissions — Medium
Opening files with excessive permissions.
Bad:
f, _ := os.OpenFile("config.json", os.O_CREATE, 0644) // DON'T: World-readable
Good:
f, _ := os.OpenFile("config.json", os.O_CREATE, 0600) // OK: Owner only
Insecure mkdir — Low
Creating directories with overly permissive permissions.
Bad:
os.MkdirAll("/var/myapp/cache", 0777) // DON'T: World-writable
Good:
os.MkdirAll("/var/myapp/cache", 0750) // OK: Group-writable
Insecure File Write Permissions — Medium
Opening files for writing with inappropriate permissions.
Bad:
os.OpenFile("app.log", os.O_CREATE, 0666) // DON'T: World-writable
Good:
os.OpenFile("app.log", os.O_CREATE|os.O_APPEND, 0640) // OK
Tainted File Read — High
Reading files based on unvalidated input.
Bad:
func readFile(filename string) ([]byte, error) {
return os.ReadFile(filename) // DON'T: No validation
}
Good (Go 1.24+):
const allowedDir = "/var/www/public/"
func readFile(filename string) ([]byte, error) {
root, err := os.OpenRoot(allowedDir)
if err != nil { return nil, err }
defer root.Close()
f, err := root.Open(filename) // cannot escape root directory
if err != nil { return nil, err }
defer f.Close()
return io.ReadAll(f)
}
Good (pre-Go 1.24 fallback):
const allowedDir = "/var/www/public/"
func readFile(filename string) ([]byte, error) {
fullPath, err := safeJoin(allowedDir, filename)
if err != nil {
return nil, err
}
return os.ReadFile(fullPath)
}
CWE References
- CWE-22: Path Traversal (Directory Traversal)
- CWE-409: Zip Bomb Decompression
- CWE-379: Insecure Temp File Creation
- CWE-732: Incorrect File Permissions