VNX-GO-027: Potential path traversal via file path construction
Detects a file opened, written, removed or served (os, ioutil, http.ServeFile, gin/echo/fiber file helpers) with a path built from a request value that was not reduced with filepath.Base, IsLocal, Rel or a prefix check first.
| Field | Value |
|---|---|
| Rule ID | VNX-GO-027 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Go |
| CWE | CWE-22 |
| CAPEC | CAPEC-128 |
| ATT&CK | T1082 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | path-traversal file directory |
| Source | internal/sast/rules/vnx-go-027.rego |
Overview
This rule flags instances where file paths are constructed using user-controlled input without proper validation or sanitization. This pattern can lead to path traversal vulnerabilities (also known as directory traversal) where attackers can access files outside of the intended directory.
This maps to CWE-22: Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’).
Why This Matters
Path traversal vulnerabilities occur when user input is used to construct file paths without proper validation, allowing attackers to navigate outside of the intended directory structure. This can lead to:
- Reading sensitive files (like
/etc/passwd, application source code, or configuration files) - Writing files to unexpected locations (potentially leading to remote code execution)
- Accessing application source code, configuration, or credentials
- Modifying or deleting critical system files
What Gets Flagged
A file operation whose path contains a request value, inline, through its variable, or through one path built from it with filepath.Join, fmt.Sprintf or concatenation.
// FLAGGED: ?name=../../etc/passwd
name := r.URL.Query().Get("name")
p := fmt.Sprintf("./configs/%s.json", name)
data, err := os.ReadFile(p)
// FLAGGED: a route parameter served directly
c.File(filepath.Join("uploads", c.Param("file")))
Not flagged: values reduced first (filepath.Base, filepath.IsLocal, filepath.Rel, strings.HasPrefix on the joined path, a .. test, SecureJoin, filepath.Clean("/"+v)), numeric IDs, files opened through os.Root, r.URL.Path given to http.ServeFile, and operator input (os.Args, environment). A bare filepath.Clean of a relative value is not treated as a check, because it keeps a leading ../.
Remediation
-
Validate and sanitize user input: Ensure user input only contains allowed characters and doesn’t contain path traversal sequences:
// SAFE: Validate input for path safety func isSafePathComponent(input string) bool { // Reject empty input if input == "" { return false } // Reject path traversal attempts if strings.Contains(input, "..") || strings.Contains(input, "/") || strings.Contains(input, "\\") { return false } // Optional: Restrict to allowed characters (alphanumeric, hyphen, underscore) return regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(input) } // Then in handler: func serveFile(w http.ResponseWriter, r *http.Request) { userPath := r.URL.Path[len("/files/"):] if !isSafePathComponent(userPath) { http.Error(w, "Invalid file name", http.StatusBadRequest) return } fullPath := filepath.Join("./uploads", userPath) // Additional safety check if !strings.HasPrefix(fullPath, "./uploads/") { http.Error(w, "Access denied", http.StatusForbidden) return } http.ServeFile(w, r, fullPath) } -
Use filepath.Clean and verify the result: Clean the path and ensure it’s still within the intended directory:
// SAFE: Use filepath.Clean and verify containment func serveFile(w http.ResponseWriter, r *http.Request) { userPath := r.URL.Path[len("/files/"):] fullPath := filepath.Join("./uploads", userPath) cleaned := filepath.Clean(fullPath) // Verify the cleaned path is still within the intended directory if !strings.HasPrefix(cleaned, "./uploads/") { http.Error(w, "Access denied", http.StatusForbidden) return } http.ServeFile(w, r, cleaned) } -
Use a file whitelist: For known files, use a map of allowed values:
// SAFE: Use whitelist for allowed files func serveFile(w http.ResponseWriter, r *http.Request) { userPath := r.URL.Path[len("/files/"):] allowed := map[string]bool{ "document1.pdf": true, "image.jpg": true, "data.csv": true, } if !allowed[userPath] { http.Error(w, "File not found", http.StatusNotFound) return } fullPath := filepath.Join("./uploads", userPath) http.ServeFile(w, r, fullPath) } -
Use URL path parameters carefully: When using routers, validate path parameters:
// SAFE: With validation in handler func serveFileHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) filename := vars["filename"] if !isSafeFilename(filename) { http.Error(w, "Invalid filename", http.StatusBadRequest) return } filepath := filepath.Join("./files", filename) // ... serve file } -
Consider using a virtual file system: For serving user-uploaded files:
- Store files with random generated names
- Keep mapping of original names to stored names in database
- Serve files by their stored names, not user-provided names
References
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)
- OWASP Application Security Verification Standard v4.0 – V5.3.2 File Path Traversal Protection
- OWASP Path Traversal Cheat Sheet
- Go filepath package documentation
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-go-027
// nosec vnx-go-027
Or record an ignore rule: vulnetix ignore add --rule vnx-go-027 --reason "...". See Suppressing findings.