{"record":{"id":"b47137224eb8ccec","repo":"joewalnes/websocketd","slug":"path-q-escapes-boundary-q","errorCode":null,"errorMessage":"path %q escapes boundary %q","messagePattern":"path %q escapes boundary %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"libwebsocketd/handler.go","lineNumber":199,"sourceCode":"\t\treturn urlInfo, nil\n\t}\n\treturn nil, fmt.Errorf(\"could not resolve script for path %q\", path)\n}\n\n// checkPathBoundary resolves symlinks and verifies the real path is within the\n// allowed directory. Returns an error if the path escapes the boundary.\nfunc checkPathBoundary(path, boundary string) error {\n\trealPath, err := filepath.EvalSymlinks(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\trealBoundary, err := filepath.EvalSymlinks(boundary)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Ensure the resolved path starts with the resolved boundary\n\tif !strings.HasPrefix(realPath, realBoundary+string(filepath.Separator)) && realPath != realBoundary {\n\t\treturn fmt.Errorf(\"path %q escapes boundary %q\", realPath, realBoundary)\n\t}\n\treturn nil\n}\n\n// generateId produces the per-connection identifier exposed as UNIQUE_ID.\n// Crypto-random rather than timestamp-derived: a UnixNano id is guessable\n// (one connection's id narrows the next one to nanoseconds) and coarse\n// enough to collide under bursts. Falls back to the timestamp only if the\n// system CSPRNG is unavailable, which is not a condition worth refusing\n// connections over.\nfunc generateId() string {\n\tb := make([]byte, 8)\n\tif _, err := rand.Read(b); err != nil {\n\t\treturn strconv.FormatInt(time.Now().UnixNano(), 10)\n\t}\n\treturn hex.EncodeToString(b)\n}\n","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/joewalnes/websocketd/blob/7a8683dc7f9778dc615945aaed2a8dc77290227b/libwebsocketd/handler.go#L181-L217","documentation":"checkPathBoundary resolves symlinks with filepath.EvalSymlinks and verifies the real path still lives inside the resolved boundary directory. If the resolved path starts outside the boundary (e.g. via a symlink pointing elsewhere), it refuses with this error — a security guard against path-traversal and symlink escape.","triggerScenarios":"A symlink inside the served script/cgi directory pointing to /etc or any path outside the base dir; a requested path whose symlink chain resolves outside the configured root; TestCgiSymlinkEscape-style setups with `ln -s /etc/passwd <dir>/evil`.","commonSituations":"Developers symlinking shared scripts from another project into the served directory; dotfile symlinks (e.g. to a home dir) inside a served folder; container images where a symlink target exists on the host but not the container path layout.","solutions":["Remove or retarget the symlink so its real destination is inside the boundary directory","Copy the target files into the boundary instead of symlinking them","Serve the actual parent directory (boundary = the real location of the files) so the symlink stays inside it"],"exampleFix":"// before\nln -s /etc/passwd ./cgi/evil        # rejected: escapes boundary\n// after\ncp /path/to/allowed/script ./cgi/   # real file inside boundary","handlingStrategy":"validation","validationCode":"real, err := filepath.EvalSymlinks(filepath.Join(servedDir, name))\nif err != nil {\n\tlog.Fatal(err)\n}\nabsBoundary, _ := filepath.EvalSymlinks(servedDir)\nif !strings.HasPrefix(real, absBoundary+string(os.PathSeparator)) {\n\tlog.Fatalf(\"symlink %s escapes served dir\", name)\n}","typeGuard":null,"tryCatchPattern":"if err := checkPathBoundary(p, baseDir); err != nil {\n\t// 'escapes boundary' — reject request or fix/remove the symlink; do not retry as-is\n}","preventionTips":["Do not symlink external files into served directories; copy them","Audit served dirs for symlinks: `find . -type l -ls`","Keep the boundary at the real parent of all served files","Treat this error as a security signal, not a bug to route around"],"tags":["security","symlink","path-traversal"],"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"}