{"id":"72465c9847701780","repo":"gorilla/mux","slug":"err-error","errorCode":null,"errorMessage":"err.Error()","messagePattern":"err\\.Error\\(\\)","errorType":"http","errorClass":null,"httpStatus":500,"severity":"warning","filePath":"README.md","lineNumber":264,"sourceCode":"// on the SPA handler. If a file is found, it will be served. If not, the\n// file located at the index path on the SPA handler will be served. This\n// is suitable behavior for serving an SPA (single page application).\nfunc (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\t// Join internally call path.Clean to prevent directory traversal\n\tpath := filepath.Join(h.staticPath, r.URL.Path)\n\n\t// check whether a file exists or is a directory at the given path\n\tfi, err := os.Stat(path)\n\tif os.IsNotExist(err) || fi.IsDir() {\n\t\t// file does not exist or path is a directory, serve index.html\n\t\thttp.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))\n\t\treturn\n\t}\n\n\tif err != nil {\n\t\t// if we got an error (that wasn't that the file doesn't exist) stating the\n\t\t// file, return a 500 internal server error and stop\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n        return\n\t}\n\n\t// otherwise, use http.FileServer to serve the static file\n\thttp.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)\n}\n\nfunc main() {\n\trouter := mux.NewRouter()\n\n\trouter.HandleFunc(\"/api/health\", func(w http.ResponseWriter, r *http.Request) {\n\t\t// an example API handler\n\t\tjson.NewEncoder(w).Encode(map[string]bool{\"ok\": true})\n\t})\n\n\tspa := spaHandler{staticPath: \"build\", indexPath: \"index.html\"}\n\trouter.PathPrefix(\"/\").Handler(spa)\n","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/gorilla/mux/blob/db9d1d0073d27a0a2d9a8c1bc52aa0af4374d265/README.md#L246-L282","documentation":"Not an error thrown by gorilla/mux. README.md:264 is part of the SPA static-file handler example: when os.Stat(filepath.Join(staticPath, r.URL.Path)) returns an error other than os.IsNotExist, the example responds http.Error(w, err.Error(), http.StatusInternalServerError). The 'error' is the leaked internal error string written to the response body — a security anti-pattern.","triggerScenarios":"os.Stat fails on the joined path for a reason other than 'file not found' — permission denied (EACCES), I/O error, stale NFS handle, ENAMETOOLONG, or read-only filesystem — and the example dumps err.Error() straight into the HTTP response.","commonSituations":"Deploying the SPA handler with a staticPath the process can't read (wrong user / umask); disk or mount failure under load; path constructed from untrusted input becoming too long; container ran as a user without read access to the assets.","solutions":["Log err with full detail server-side, then write a generic message (e.g. \"internal error\") to the client — never err.Error().","Check the staticPath directory permissions and the UID/GID the server runs as; ensure read+execute on every parent directory.","Handle specific syscall errors explicitly (e.g. os.IsPermission, errors.Is(err, syscall.ENAMETOOLONG)) and return 403/414 as appropriate instead of 500.","Add a bounded Allow list or canonicalize with filepath.Clean and reject '..' traversal before stating, so attacker-controlled paths can't surface OS errors."],"exampleFix":"// before (README.md:264)\nif err != nil {\n    http.Error(w, err.Error(), http.StatusInternalServerError)\n    return\n}\n\n// after: log full detail, return generic body\nif err != nil {\n    log.Printf(\"spa stat %q: %v\", path, err)\n    if errors.Is(err, os.ErrPermission) {\n        http.Error(w, \"forbidden\", http.StatusForbidden)\n    } else {\n        http.Error(w, \"internal error\", http.StatusInternalServerError)\n    }\n    return\n}","handlingStrategy":"validation","validationCode":"// Validate the joined path and permissions BEFORE os.Stat to avoid leaking OS errors.\nfunc safeSpaPath(root, reqPath, index string) (string, error) {\n    cleaned := filepath.Clean(\"/\" + reqPath)            // force under root\n    full := filepath.Join(root, cleaned)\n    if !strings.HasPrefix(full, filepath.Clean(root)+string(os.PathSeparator)) && full != filepath.Clean(root) {\n        return \"\", os.ErrPermission                      // traversal attempt\n    }\n    return full, nil\n}\n\nfull, err := safeSpaPath(h.staticPath, r.URL.Path, h.indexPath)\nif err != nil {\n    http.Error(w, \"forbidden\", http.StatusForbidden)\n    return\n}\nfi, err := os.Stat(full)","typeGuard":null,"tryCatchPattern":"// After os.Stat: classify, log full detail server-side, send generic body.\nfi, err := os.Stat(full)\nif os.IsNotExist(err) || (err == nil && fi.IsDir()) {\n    http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))\n    return\n}\nif err != nil {\n    log.Printf(\"spa stat %q: %v\", full, err) // full detail to logs only\n    switch {\n    case errors.Is(err, os.ErrPermission):\n        http.Error(w, \"forbidden\", http.StatusForbidden)\n    default:\n        http.Error(w, \"internal error\", http.StatusInternalServerError) // NOT err.Error()\n    }\n    return\n}","preventionTips":["Never write err.Error() to an http.Response — log it server-side, return a generic message to the client.","Run the server under the UID/GID that owns staticPath and verify read+execute bits on every parent directory.","Canonicalize and bound-check joined paths (filepath.Clean + HasPrefix root) before stating to prevent traversal and ENAMETOOLONG.","Add a test that chmods staticPath to 000 and asserts the handler returns 403/500 with a non-leaking body."],"tags":["security","static-files","error-handling","information-disclosure","example","gorilla-mux"],"analyzedSha":"db9d1d0073d27a0a2d9a8c1bc52aa0af4374d265","analyzedAt":"2026-08-04T21:35:47.097Z","schemaVersion":2}