VYPR
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.

PackageAffected versionsPatched versions
github.com/patrickhener/goshsGo
< 1.1.5-0.20260401172448-237f3af891a91.1.5-0.20260401172448-237f3af891a9

Affected products

3
  • Goshs/Goshs3 versions
    cpe:2.3:a:goshs:goshs:2.0.0:beta1:*:*:*:go:*:*+ 2 more
    • cpe:2.3:a:goshs:goshs:2.0.0:beta1:*:*:*:go:*:*
    • cpe:2.3:a:goshs:goshs:2.0.0:beta2:*:*:*:go:*:*
    • cpe:2.3:a:goshs:goshs:*:*:*:*:*:go:*:*range: <2.0.0

Patches

1
237f3af891a9

Fix security issues reported at https://github.com/patrickhener/goshs/security/advisories/GHSA-6qcc-6q27-whp8 and sanitize paths in general throughout all handlers

https://github.com/patrickhener/goshsPatrick HenerApr 1, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.