[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
# 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:**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
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):**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
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):**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
gr, _ := gzip.NewReader(f)
|
||||
out, _ := os.Create(dst)
|
||||
io.Copy(out, gr) // DON'T: No size limits
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
f, _ := os.Create("/tmp/myapp.temp") // DON'T: Predictable name
|
||||
f.WriteString(data)
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
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:**
|
||||
|
||||
```go
|
||||
f, _ := os.OpenFile("config.json", os.O_CREATE, 0644) // DON'T: World-readable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
f, _ := os.OpenFile("config.json", os.O_CREATE, 0600) // OK: Owner only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure mkdir — Low
|
||||
|
||||
Creating directories with overly permissive permissions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
os.MkdirAll("/var/myapp/cache", 0777) // DON'T: World-writable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
os.MkdirAll("/var/myapp/cache", 0750) // OK: Group-writable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Insecure File Write Permissions — Medium
|
||||
|
||||
Opening files for writing with inappropriate permissions.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
os.OpenFile("app.log", os.O_CREATE, 0666) // DON'T: World-writable
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```go
|
||||
os.OpenFile("app.log", os.O_CREATE|os.O_APPEND, 0640) // OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tainted File Read — High
|
||||
|
||||
Reading files based on unvalidated input.
|
||||
|
||||
**Bad:**
|
||||
|
||||
```go
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
return os.ReadFile(filename) // DON'T: No validation
|
||||
}
|
||||
```
|
||||
|
||||
**Good (Go 1.24+):**
|
||||
|
||||
```go
|
||||
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):**
|
||||
|
||||
```go
|
||||
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
|
||||
Reference in New Issue
Block a user