Critical severity9.8NVD Advisory· Published Apr 6, 2026· Updated Apr 9, 2026
CVE-2026-35471
CVE-2026-35471
Description
goshs is a SimpleHTTPServer written in Go. Prior to 2.0.0-beta.3, tdeleteFile() missing return after path traversal check. This vulnerability is fixed in 2.0.0-beta.3.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/patrickhener/goshsGo | < 1.1.5-0.20260401172448-237f3af891a9 | 1.1.5-0.20260401172448-237f3af891a9 |
Affected products
3Patches
1237f3af891a9Fix security issues reported at https://github.com/patrickhener/goshs/security/advisories/GHSA-6qcc-6q27-whp8 and sanitize paths in general throughout all handlers
3 files changed · +77 −60
httpserver/handler.go+37 −28 modified@@ -231,18 +231,21 @@ func (fs *FileServer) handler(w http.ResponseWriter, req *http.Request) { json = true } - // Get url so you can extract Headline and title - upath := req.URL.Path - // Ignore default browser call to /favicon.ico - if upath == "/favicon.ico" { + if req.URL.Path == "/favicon.ico" { return } - upath = filepath.FromSlash(filepath.Clean("/" + strings.Trim(upath, "/"))) - - // Define absolute path - open := fs.Webroot + upath + open, err := sanitizePath(fs.Webroot, req.URL.Path) + if err != nil { + fs.handleError(w, req, err, http.StatusBadRequest) + return + } + // Relative path used by templates + upath := strings.TrimPrefix(open, filepath.Clean(fs.Webroot)) + if upath == "" { + upath = "/" + } // Check if you are in a dir // disable G304 (CWE-22): Potential file inclusion via variable @@ -674,21 +677,15 @@ func (fs *FileServer) socket(w http.ResponseWriter, req *http.Request) { // deleteFile will delete a file func (fs *FileServer) deleteFile(w http.ResponseWriter, req *http.Request) { - // Get path - upath := filepath.FromSlash(filepath.Clean("/" + strings.Trim(req.URL.Path, "/"))) - - fileCleaned, _ := url.QueryUnescape(upath) - if strings.Contains(fileCleaned, "..") { - w.WriteHeader(500) - _, err := w.Write([]byte("Cannot delete file")) - if err != nil { - logger.Errorf("error writing answer to client: %+v", err) - } + deletePath, err := sanitizePath(fs.Webroot, req.URL.Path) + if err != nil { + http.Error(w, "Cannot delete file", http.StatusBadRequest) + body := fs.emitCollabEvent(req, http.StatusBadRequest) + logger.LogRequest(req, http.StatusBadRequest, fs.Verbose, fs.Webhook, body) + return } - deletePath := filepath.Join(fs.Webroot, fileCleaned) - - err := os.RemoveAll(deletePath) + err = os.RemoveAll(deletePath) if err != nil { logger.Warnf("error removing %+v", deletePath) } @@ -714,7 +711,17 @@ func (fs *FileServer) CreateShareHandler(w http.ResponseWriter, r *http.Request) return } - upath := filepath.FromSlash(filepath.Clean("/" + strings.Trim(r.URL.Path, "/"))) + fpath, err := sanitizePath(fs.Webroot, r.URL.Path) + if err != nil { + body := fs.emitCollabEvent(r, http.StatusBadRequest) + logger.LogRequest(r, http.StatusBadRequest, fs.Verbose, fs.Webhook, body) + http.Error(w, "Invalid path", http.StatusBadRequest) + return + } + upath := strings.TrimPrefix(fpath, filepath.Clean(fs.Webroot)) + if upath == "" { + upath = "/" + } var expires time.Time var downloadLimit int @@ -749,7 +756,6 @@ func (fs *FileServer) CreateShareHandler(w http.ResponseWriter, r *http.Request) } // Get stat for file - fpath := filepath.Join(fs.Webroot, upath) stat, err = os.Stat(fpath) if err != nil { logger.Errorf("cannot get stat information for file: %s", fpath) @@ -900,12 +906,15 @@ func (fs *FileServer) handleMkdir(w http.ResponseWriter, r *http.Request) { return } - // Get path - upath := filepath.FromSlash(filepath.Clean("/" + strings.Trim(r.URL.Path, "/"))) - finalPath := filepath.Join(fs.Webroot, upath) + // Get and sanitize path + finalPath, err := sanitizePath(fs.Webroot, r.URL.Path) + if err != nil { + http.Error(w, "Invalid path", http.StatusBadRequest) + return + } - // Create directory upath - err := os.MkdirAll(finalPath, 0755) + // Create directory + err = os.MkdirAll(finalPath, 0755) if err != nil { body := fs.emitCollabEvent(r, http.StatusInternalServerError) logger.LogRequest(r, http.StatusInternalServerError, fs.Verbose, fs.Webhook, body)
httpserver/helper.go+20 −0 modified@@ -8,13 +8,33 @@ import ( "fmt" "io/fs" "net/http" + "net/url" "os" + "path/filepath" "strings" "github.com/patrickhener/goshs/logger" "github.com/skip2/go-qrcode" ) +// sanitizePath validates that requestPath stays within root after decoding and +// cleaning. It returns the absolute path on success, or an error if the path +// would escape root (path traversal). +func sanitizePath(root, requestPath string) (string, error) { + decoded, err := url.QueryUnescape(requestPath) + if err != nil { + // Malformed percent-encoding — use raw value; filepath.Clean will handle it. + decoded = requestPath + } + clean := filepath.Clean("/" + strings.TrimLeft(decoded, "/")) + abs := filepath.Join(root, clean) + rootClean := filepath.Clean(root) + if abs != rootClean && !strings.HasPrefix(abs, rootClean+string(filepath.Separator)) { + return "", fmt.Errorf("path escapes root: %q", requestPath) + } + return abs, nil +} + func removeItem(sSlice []item, item string) []item { index := 0
httpserver/updown.go+20 −32 modified@@ -7,9 +7,7 @@ import ( "fmt" "io" "net/http" - "net/url" "os" - "path" "path/filepath" "strings" "time" @@ -23,18 +21,11 @@ func (fs *FileServer) put(w http.ResponseWriter, req *http.Request) { fs.handleError(w, req, fmt.Errorf("%s", "Upload not allowed due to 'read only' option"), http.StatusForbidden) return } - // Get url so you can extract Headline and title - upath := req.URL.Path - - filename := strings.Split(upath, "/") - outName := filename[len(filename)-1] - - // construct target path - targetpath := strings.Split(upath, "/") - targetpath = targetpath[:len(targetpath)-1] - target := strings.Join(targetpath, "/") - - savepath := fmt.Sprintf("%s%s/%s", fs.UploadFolder, target, outName) + savepath, err := sanitizePath(fs.UploadFolder, req.URL.Path) + if err != nil { + fs.handleError(w, req, err, http.StatusBadRequest) + return + } body, err := io.ReadAll(req.Body) if err != nil { @@ -74,13 +65,13 @@ func (fs *FileServer) upload(w http.ResponseWriter, req *http.Request) { fs.handleError(w, req, fmt.Errorf("%s", "Upload not allowed due to 'read only' option"), http.StatusForbidden) return } - // Get url so you can extract Headline and title - upath := req.URL.Path - - // construct target path - targetpath := strings.Split(upath, "/") - targetpath = targetpath[:len(targetpath)-1] - target := strings.Join(targetpath, "/") + // Derive and sanitize the target directory (strip trailing "/upload" from URL). + upathDir := strings.TrimSuffix(req.URL.Path, "/upload") + targetDir, err := sanitizePath(fs.UploadFolder, upathDir) + if err != nil { + fs.handleError(w, req, err, http.StatusBadRequest) + return + } reader, err := req.MultipartReader() if err != nil { @@ -106,7 +97,7 @@ func (fs *FileServer) upload(w http.ResponseWriter, req *http.Request) { filenameClean := filenameSlice[len(filenameSlice)-1] // Prepare destination file paths - finalPath := fmt.Sprintf("%s%s/%s", fs.UploadFolder, target, filenameClean) + finalPath := filepath.Join(targetDir, filenameClean) tempPath := finalPath + "~" // Create temp file @@ -170,7 +161,7 @@ func (fs *FileServer) upload(w http.ResponseWriter, req *http.Request) { logger.LogRequest(req, http.StatusOK, fs.Verbose, fs.Webhook, body) // Redirect back from where we came from - http.Redirect(w, req, target, http.StatusSeeOther) + http.Redirect(w, req, upathDir, http.StatusSeeOther) } // bulkDownload will provide zip archived download bundle of multiple selected files @@ -188,16 +179,13 @@ func (fs *FileServer) bulkDownload(w http.ResponseWriter, req *http.Request) { fs.handleError(w, req, errors.New("you need to select a file before you can download a zip archive"), 404) } - // Clean file paths and fill slice - // Also sanitize path (No path traversal) - // If .. in single string just skip file + // Validate each path and collect absolute paths; skip any traversal attempts for _, file := range files { - fileCleaned, _ := url.QueryUnescape(file) - if strings.Contains(fileCleaned, "..") { - // Just skip this file + absPath, err := sanitizePath(fs.Webroot, file) + if err != nil { continue } - filesCleaned = append(filesCleaned, fileCleaned) + filesCleaned = append(filesCleaned, absPath) } // Construct filename to download @@ -256,9 +244,9 @@ func (fs *FileServer) bulkDownload(w http.ResponseWriter, req *http.Request) { return nil } - // Loop over files and add to zip + // Loop over files and add to zip (filesCleaned contains validated absolute paths) for _, file := range filesCleaned { - err := filepath.Walk(path.Join(fs.Webroot, file), walker) + err := filepath.Walk(file, walker) if err != nil { logger.Errorf("creating zip file: %+v", err) }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/patrickhener/goshs/security/advisories/GHSA-6qcc-6q27-whp8nvdExploitMitigationVendor AdvisoryWEB
- github.com/advisories/GHSA-6qcc-6q27-whp8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-35471ghsaADVISORY
- github.com/patrickhener/goshs/commit/237f3af891a90df9b903b85f1cd3438040ca261aghsaWEB
News mentions
0No linked articles in our index yet.