{"record":{"id":"49f72c9909f0b7ed","repo":"joewalnes/websocketd","slug":"path-q-escapes-directory-q","errorCode":null,"errorMessage":"path %q escapes directory %q","messagePattern":"path %q escapes directory %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"libwebsocketd/http.go","lineNumber":220,"sourceCode":"\t// The rooted clean above should already guarantee this on every OS\n\t// (including Windows, where ToSlash folds \"..\\\" into \"../\" before the\n\t// clean sees it), so this check is not load-bearing today — it is here\n\t// to fail closed if the normalization above is ever weakened.\n\tif err := containsPath(cgiDir, filePath); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filePath, nil\n}\n\n// containsPath reports an error unless child is dir itself or lies beneath it,\n// comparing lexically (no filesystem access, no symlink resolution).\nfunc containsPath(dir, child string) error {\n\trel, err := filepath.Rel(dir, child)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\treturn fmt.Errorf(\"path %q escapes directory %q\", child, dir)\n\t}\n\treturn nil\n}\n\n// serveCGI executes CGI scripts from the configured directory. Returns true if handled.\nfunc (h *WebsocketdServer) serveCGI(w http.ResponseWriter, req *http.Request, log *LogScope) bool {\n\tif h.Config.CgiDir == \"\" {\n\t\treturn false\n\t}\n\tfilePath, err := resolveCgiPath(h.Config.CgiDir, req.URL.Path)\n\tif err != nil {\n\t\tlog.Access(\"http\", \"CGI: %s\", err)\n\t\treturn false\n\t}\n\tfi, err := os.Stat(filePath)\n\tif err != nil || fi.IsDir() {\n\t\treturn false\n\t}","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/joewalnes/websocketd/blob/7a8683dc7f9778dc615945aaed2a8dc77290227b/libwebsocketd/http.go#L202-L238","documentation":"containsPath computes filepath.Rel between the allowed directory and a candidate child path; if the relative path is '..' or starts with '../', the child is outside the directory and the error is returned. It is a lexical containment check used by resolveCgiPath as defense-in-depth against traversal outside the CGI directory.","triggerScenarios":"A joined path like filepath.Join(cgiDir, '../secrets') that Rel shows escaping the dir; calling resolveCgiPath/containsPath with a child built from an unrooted or attacker-controlled URL path containing '..' segments that survive normalization.","commonSituations":"Hand-rolled path joining from user input without rooting/cleaning first; refactors that bypass the Clean('/'+path) normalization; tests (TestCgiSymlinkEscape) exercising escape attempts.","solutions":["Normalize untrusted input before joining: path.Clean('/'+filepath.ToSlash(userPath)) so '..' climbs fold to root","Ensure the child is built by filepath.Join(dir, cleanedName) rather than raw string concatenation","If you see this in legit use, the child genuinely lies outside dir — fix the caller's path construction, not the check"],"exampleFix":"// before\nfilePath := cgiDir + \"/\" + urlPath            // '..' survives\n// after\nclean := path.Clean(\"/\" + filepath.ToSlash(urlPath))\nfilePath := filepath.Join(cgiDir, filepath.FromSlash(clean))","handlingStrategy":"validation","validationCode":"clean := path.Clean(\"/\" + filepath.ToSlash(userPath))\nif strings.Contains(clean, \"..\") {\n\treturn fmt.Errorf(\"rejecting traversal attempt: %q\", userPath)\n}\nchild := filepath.Join(cgiDir, filepath.FromSlash(clean))","typeGuard":"func safeChild(dir, userPath string) (string, bool) {\n\tclean := path.Clean(\"/\" + filepath.ToSlash(userPath))\n\tchild := filepath.Join(dir, filepath.FromSlash(clean))\n\trel, err := filepath.Rel(dir, child)\n\tif err != nil || rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\treturn \"\", false\n\t}\n\treturn child, true\n}","tryCatchPattern":"if err := containsPath(dir, child); err != nil {\n\t// 'escapes directory' — log the rejected input and return 4xx; never retry same input\n}","preventionTips":["Root and Clean all user-supplied paths before joining: path.Clean(\"/\"+p)","Build child paths with filepath.Join, never string concatenation","Keep the containsPath check as final defense; do not bypass it in refactors","Add traversal-attempt tests (../../etc/passwd) to your handler suite"],"tags":["security","path-traversal","cgi"],"backgroundTag":"path-traversal-escape","analyzedSha":"7a8683dc7f9778dc615945aaed2a8dc77290227b","analyzedAt":"2026-09-03T13:52:22.309Z","contentChangedAt":"2026-09-03T13:52:22.309Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}