VNX-GO-026: Missing file type validation on upload
Detects an uploaded multipart file saved to disk under the client's own file name (FileHeader.Filename) by a handler that never checks the file's extension or content type.
| Field | Value |
|---|---|
| Rule ID | VNX-GO-026 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Go |
| CWE | CWE-434 |
| CAPEC | CAPEC-177 |
| ATT&CK | T1105 |
| 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 | file-upload validation mime-type |
| Source | internal/sast/rules/vnx-go-026.rego |
Overview
This rule flags instances where file upload functionality is implemented using Go’s standard library multipart form handling but lacks apparent file type validation. Accepting file uploads without validating the file type can lead to malicious file upload vulnerabilities where attackers upload executable code, scripts, or other dangerous files.
This maps to CWE-434: Unrestricted Upload of File with Dangerous Type.
Why This Matters
File upload functionality is a common feature in web applications, but it presents significant security risks if not properly implemented. Without file type validation, attackers can upload:
- Malicious scripts (.php, .py, .sh) that can be executed on the server
- Executable files (.exe, .bin) that can compromise the server
- HTML/JavaScript files that can lead to XSS when served to users
- Files with dangerous extensions that can bypass client-side validation
These vulnerabilities can lead to remote code execution, data theft, server compromise, or use of the server as a malware distribution point.
What Gets Flagged
A handler that receives an uploaded file and saves it under the name the client sent, with no check anywhere in the handler of what kind of file it is.
// FLAGGED: the client picks the extension (shell.php, index.html)
file, handler, err := r.FormFile("upload")
dst, err := os.Create(filepath.Join("./public/uploads", handler.Filename))
io.Copy(dst, file)
// FLAGGED: gin
f, _ := c.FormFile("avatar")
c.SaveUploadedFile(f, "uploads/"+f.Filename)
Not flagged: handlers that check filepath.Ext, strings.HasSuffix, http.DetectContentType, the part’s Content-Type, image.DecodeConfig or call a helper such as validateUpload; files saved under a server-generated name; uploads parsed in memory; clients building a multipart request.
Remediation
-
Validate MIME type: Use Go’s
net/http.DetectContentTypeormimepackage to verify the actual file content:// SAFE: Validate MIME type func uploadHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "Unable to parse form", http.StatusBadRequest) return } file, handler, err := r.FormFile("upload") if err != nil { http.Error(w, "Invalid file", http.StatusBadRequest) return } defer file.Close() // Read first 512 bytes to detect content type buffer := make([]byte, 512) _, err := file.Read(buffer) if err != nil { http.Error(w, "Unable to read file", http.StatusInternalServerError) return } // Reset file pointer for later use file.Seek(0, io.SeekStart) contentType := http.DetectContentType(buffer) allowedTypes := map[string]bool{ "image/jpeg": true, "image/png": true, "application/pdf": true, } if !allowedTypes[contentType] { http.Error(w, "Invalid file type", http.StatusBadRequest) return } // Now safe to save the file dst, err := os.Create(filepath.Join("./uploads", handler.Filename)) // ... rest of upload logic } -
Validate file extension: Check that the file extension matches expected types:
// SAFE: Validate file extension func isAllowedExtension(filename string) bool { ext := strings.ToLower(filepath.Ext(filename)) allowed := map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".pdf": true, ".txt": true, } return allowed[ext] } // Then in upload handler: if !isAllowedExtension(handler.Filename) { http.Error(w, "Invalid file extension", http.StatusBadRequest) return } -
Combine multiple validation approaches: Use both MIME type and extension validation:
// SAFE: Defense in depth - check both content and extension func validateFile(file multipart.File, filename string) error { // Check extension ext := strings.ToLower(filepath.Ext(filename)) if !isAllowedExtension(ext) { return fmt.Errorf("invalid file extension: %s", ext) } // Check content type buffer := make([]byte, 512) if _, err := file.Read(buffer); err != nil { return fmt.Errorf("unable to read file: %v", err) } file.Seek(0, io.SeekStart) // Reset for later use contentType := http.DetectContentType(buffer) if !isAllowedMIMEType(contentType) { return fmt.Errorf("invalid file type: %s", contentType) } // Optional: Additional validation like magic bytes for specific types return nil } -
Implement file quarantine and scanning: For high-security applications:
- Save uploaded files to a quarantine area outside web root
- Scan with antivirus software
- Validate file content matches expected format (e.g., use image libraries to verify real images)
- Only move to public location after passing all validations
References
- CWE-434: Unrestricted Upload of File with Dangerous Type
- OWASP Application Security Verification Standard v4.0 – V5.2.2 File Type Verification
- OWASP File Upload Cheat Sheet
- Go net/http package documentation
- Go mime 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-026
// nosec vnx-go-026
Or record an ignore rule: vulnetix ignore add --rule vnx-go-026 --reason "...". See Suppressing findings.