{"id":"b8eb09638475f27e","repo":"labstack/echo","slug":"failed-to-unescape-path-variable-w","errorCode":null,"errorMessage":"failed to unescape path variable: %w","messagePattern":"failed to unescape path variable: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"echo.go","lineNumber":660,"sourceCode":"\n// StaticDirectoryHandler creates handler function to serve files from provided file system\n// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.\n//\n// Note: when disablePathUnescaping=false, the handler decodes the wildcard param before serving.\n// If route guards (e.g. e.GET(\"/admin/*\", forbidden)) are used to restrict parts of the\n// filesystem, an encoded separator (%2F) or encoded dot-dot (%2E%2E) in the URL can resolve to\n// a path that the router never matched against the guard route. Enabling\n// RouterConfig.UseEscapedPathForMatching does NOT fix this — it changes which path the router\n// uses for matching but still lets path.Clean resolve \"..\" segments into a guarded directory.\n// Do not rely on route guards alone to restrict a filesystem served by this handler.\n// See https://github.com/labstack/echo/security/advisories/GHSA-vfp3-v2gw-7wfq\nfunc StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {\n\treturn func(c *Context) error {\n\t\tp := c.Param(\"*\")\n\t\tif !disablePathUnescaping { // when router is already unescaping we do not want to do is twice\n\t\t\ttmpPath, err := url.PathUnescape(p)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unescape path variable: %w\", err)\n\t\t\t}\n\t\t\tp = tmpPath\n\t\t}\n\n\t\t// fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/`\n\t\t// as invalid\n\t\t// Use path.Clean (not filepath.Clean): fs.FS paths are always forward-slash, so a backslash must stay a literal\n\t\t// character rather than being interpreted as a separator on Windows (which would resolve a file across a boundary\n\t\t// the router never matched on, the same Windows backslash traversal class as GHSA-pgvm-wxw2-hrv9).\n\t\tname := path.Clean(strings.TrimPrefix(p, \"/\"))\n\t\tfi, err := fs.Stat(fileSystem, name)\n\t\tif err != nil {\n\t\t\treturn ErrNotFound\n\t\t}\n\n\t\t// If the request is for a directory and does not end with \"/\" redirect to path which ends with \"/\"\n\t\tp = c.Request().URL.Path\n\t\tif fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {","sourceCodeStart":642,"sourceCodeEnd":678,"githubUrl":"https://github.com/labstack/echo/blob/05489dc1730161df26b72d1ae2a3ba6fb8178fc7/echo.go#L642-L678","documentation":"Returned by StaticDirectoryHandler (echo.go:657-661) when url.PathUnescape of the wildcard path param fails. The client sent a %-sequence that is not valid percent-encoding (e.g. %zz or a truncated %2). The handler only runs this unescape when disablePathUnescaping is false.","triggerScenarios":"A request to a static route whose wildcard segment contains malformed percent-encoding, while disablePathUnescaping is false (the default behavior).","commonSituations":"Bots/scanners sending crafted URLs; clients with buggy URL encoders; double-encoding mishaps that produce invalid sequences.","solutions":["Let the default HTTPErrorHandler turn this into a 404/400","Override e.HTTPErrorHandler to map the error to a clean 400 and avoid echoing raw path bytes","If serving untrusted paths, review whether disablePathUnescaping or router-level unescaping fits your threat model"],"exampleFix":"// before: rely on the default handler, which leaks detail\n// after: custom error handler masks the cause\ne.HTTPErrorHandler = func(err error, c echo.Context) {\n    if strings.Contains(err.Error(), \"unescape path variable\") {\n        _ = c.String(http.StatusBadRequest, \"bad path encoding\")\n        return\n    }\n    c.DefaultHTTPErrorHandler(err, c)\n}","handlingStrategy":"try-catch","validationCode":"// nothing to validate pre-handler; reject malformed escapes at the edge (reverse proxy / WAF) when possible","typeGuard":null,"tryCatchPattern":"e.HTTPErrorHandler = func(err error, c echo.Context) {\n    if errors.Is(err, echo.ErrNotFound) || strings.Contains(err.Error(), \"unescape path variable\") {\n        _ = c.String(http.StatusBadRequest, \"bad request\")\n        return\n    }\n    c.DefaultHTTPErrorHandler(err, c)\n}","preventionTips":["Sanitise URLs at the reverse proxy","Do not log or echo raw untrusted path bytes","Use a custom HTTPErrorHandler to hide detail"],"tags":["static-files","url-encoding","security"],"analyzedSha":"05489dc1730161df26b72d1ae2a3ba6fb8178fc7","analyzedAt":"2026-08-04T21:32:47.783Z","schemaVersion":2}