VNX-GO-018: Go arbitrary file write via os.WriteFile/os.Create with user-controlled path
Detects files created, written, renamed or linked (os.Create, os.WriteFile, os.OpenFile with a write flag, os.MkdirAll, os.Rename, gin SaveUploadedFile) at a path built from HTTP request data or a multipart filename, with no filepath.Base, IsLocal, prefix check on the cleaned path or os.Root confining it. Read-only opens and server-chosen names are not reported.
| Field | Value |
|---|---|
| Rule ID | VNX-GO-018 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Go |
| CWE | CWE-22 |
| CAPEC | CAPEC-139 |
| ATT&CK | T1083 |
| 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-write go |
| Source | internal/sast/rules/vnx-go-018.rego |
Overview
This rule detects calls to os.WriteFile(), os.Create(), or ioutil.WriteFile() where the path argument may be derived from user-controlled HTTP request sources, such as r.URL, r.FormValue(), r.PathValue(), route parameters from chi, gorilla/mux, gin, or echo, within a surrounding code window of roughly 20 lines, and where no filepath.Clean() or filepath.Rel() call is present to sanitise the path. An attacker who can control the file path argument can supply ../ sequences or absolute paths to write files outside the intended directory, potentially overwriting executables, configuration files, or credentials. This maps to CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).
Path traversal in file write operations is significantly more severe than path traversal in read operations. While a read traversal leaks data, a write traversal can achieve remote code execution by overwriting interpreted scripts, web server configuration, or process init files. CAPEC-139 (Relative Path Traversal) and MITRE ATT&CK T1083 (File and Directory Discovery) document the broader family of path manipulation techniques.
Why This Matters
File upload handlers are a recurring source of path traversal vulnerabilities. A common pattern in Go web servers is to take a filename from a multipart form field, construct a path by joining it with an upload directory, and write the file. If the filename is used directly without sanitisation, a value such as "../../etc/cron.d/backdoor" will write outside the upload directory. Even without ../ sequences, an absolute path like /etc/passwd can overwrite critical system files if the process runs as root or with elevated privileges.
Go’s filepath.Clean() function resolves . and .. elements and removes redundant separators, but it does not confine the result to a base directory. After cleaning, the path must be checked with strings.HasPrefix(cleanedPath, baseDir) or filepath.Rel() to confirm it remains within the intended directory. Forgetting the prefix check after calling filepath.Clean() is itself a common mistake.
What Gets Flagged
// FLAGGED: user-supplied filename written directly to disk
func uploadHandler(w http.ResponseWriter, r *http.Request) {
filename := r.FormValue("filename")
path := filepath.Join("/var/uploads", filename) // traversal risk
os.WriteFile(path, data, 0644)
}
// FLAGGED: path built from URL parameter without cleaning
func saveHandler(c *gin.Context) {
name := c.Param("name")
os.Create("/data/" + name) // traversal: name = "../../etc/crontab"
}
Remediation
-
Clean the user-supplied path component and verify it stays within the base directory before any file operation.
// SAFE: clean and confirm the path stays within the upload directory const uploadDir = "/var/uploads" func uploadHandler(w http.ResponseWriter, r *http.Request) { filename := filepath.Base(r.FormValue("filename")) // remove directory components cleanPath := filepath.Join(uploadDir, filename) cleanPath = filepath.Clean(cleanPath) if !strings.HasPrefix(cleanPath, uploadDir+string(filepath.Separator)) { http.Error(w, "invalid filename", http.StatusBadRequest) return } if err := os.WriteFile(cleanPath, data, 0644); err != nil { http.Error(w, "write failed", http.StatusInternalServerError) } } -
Use
filepath.Base()to strip all directory components from a user-supplied filename before joining it with the base path. This prevents any traversal sequences.// SAFE: filepath.Base strips all path components leaving only the filename func saveFile(baseDir, userFilename string, data []byte) error { safe := filepath.Join(baseDir, filepath.Base(userFilename)) return os.WriteFile(safe, data, 0600) } -
Generate server-side filenames instead of trusting user input for the filename at all. Store the user-provided name as metadata in a database record.
// SAFE: server-generated filename, user name stored in database only func uploadHandler(w http.ResponseWriter, r *http.Request) { id := uuid.New().String() path := filepath.Join("/var/uploads", id) os.WriteFile(path, data, 0600) db.Store(id, r.FormValue("filename")) // original name in DB, not on disk }
References
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CAPEC-139: Relative Path Traversal
- MITRE ATT&CK T1083 – File and Directory Discovery
- Go documentation – filepath.Clean
- OWASP Path Traversal
- OWASP Go-SCP – File handling
- Go security best practices
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-018
// nosec vnx-go-018
Or record an ignore rule: vulnetix ignore add --rule vnx-go-018 --reason "...". See Suppressing findings.