Files

6.1 KiB

Filesystem Security Rules

Filesystem vulnerabilities can lead to unauthorized file access, data leakage, and denial-of-service attacks.

Rules:

  1. User-controlled file paths MUST be confined to an allowed root.
  2. os.Root SHOULD be used for scoped file access (Go 1.24+).
  3. Zip extraction MUST check for ZipSlip path traversal.
  4. Temporary files MUST use os.CreateTemp — NEVER predictable names.
  5. 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