{"record":{"id":"4e73d8c520488406","repo":"gastownhall/beads","slug":"path-escapes-workspace-s","errorCode":null,"errorMessage":"path escapes workspace: %s","messagePattern":"path escapes workspace: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/doctor/fix/common.go","lineNumber":121,"sourceCode":"func safeWorkspacePath(root, relPath string) (string, error) {\n\tabsRoot, err := filepath.Abs(root)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid workspace path: %w\", err)\n\t}\n\n\tcleanRel := filepath.Clean(relPath)\n\tif filepath.IsAbs(cleanRel) {\n\t\treturn \"\", fmt.Errorf(\"expected relative path, got absolute: %s\", relPath)\n\t}\n\n\tjoined := filepath.Join(absRoot, cleanRel)\n\trel, err := filepath.Rel(absRoot, joined)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to resolve path: %w\", err)\n\t}\n\n\tif rel == \"..\" || strings.HasPrefix(rel, \"..\"+string(os.PathSeparator)) {\n\t\treturn \"\", fmt.Errorf(\"path escapes workspace: %s\", relPath)\n\t}\n\n\treturn joined, nil\n}\n\n// isWithinWorkspace reports whether candidate resides within the workspace root.\nfunc isWithinWorkspace(root, candidate string) bool {\n\tcleanRoot, err := filepath.Abs(root)\n\tif err != nil {\n\t\treturn false\n\t}\n\tcleanCandidate := filepath.Clean(candidate)\n\trel, err := filepath.Rel(cleanRoot, cleanCandidate)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn rel == \".\" || (rel != \"..\" && !strings.HasPrefix(rel, \"..\"+string(os.PathSeparator)))\n}","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/doctor/fix/common.go#L103-L139","documentation":"This is the workspace-escape (path traversal) guard in safeWorkspacePath. If the resolved path relative to root is \"..\" or starts with \"../\", the requested relPath would land outside the workspace, so it is rejected. This protects doctor fix operations from reading/writing arbitrary files via inputs like \"../../etc/passwd\".","triggerScenarios":"Calling safeWorkspacePath with relPath containing traversal segments, e.g. \"../secrets\", \"a/../../b\", or any input whose Join+Rel result escapes root.","commonSituations":"Hostile or corrupted input (user data, issue text, config values) used as a filename; a caller assuming symlinks or \"..\" segments are acceptable; migrating code that previously used filepath.Join directly without containment checks.","solutions":["Provide a genuinely workspace-relative path that stays inside root","Clean the input and strip/reject any \"..\" segments before calling (filepath.Clean alone is not enough)","If the target must be outside the workspace, use an explicit, separately-audited absolute-path API instead of this containment-checked helper","If this fires on legitimate input, check for symlinks in root that resolve outside the workspace (Join/Rel is lexical, not symlink-aware)"],"exampleFix":"// before\nsafeWorkspacePath(root, \"../../etc/passwd\") // error\n// after\nsafeWorkspacePath(root, filepath.Clean(strings.TrimPrefix(input, root+\"/\"))) // or reject input containing \"..\"","handlingStrategy":"validation","validationCode":"clean := filepath.Clean(input)\nif strings.Contains(clean, \"..\") || filepath.IsAbs(clean) {\n    return fmt.Errorf(\"unsafe path: %s\", input)\n}","typeGuard":"func isInsideWorkspace(root, candidate string) bool {\n    rel, err := filepath.Rel(root, filepath.Clean(candidate))\n    if err != nil { return false }\n    return rel == \".\" || (rel != \"..\" && !strings.HasPrefix(rel, \"..\"+string(os.PathSeparator)))\n}","tryCatchPattern":"p, err := safeWorkspacePath(root, rel)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"path escapes workspace\") {\n        return fmt.Errorf(\"refusing unsafe path %q\", rel)\n    }\n    return err\n}","preventionTips":["Sanitize any externally sourced filename: reject \"..\" segments early","Remember Clean/Join is lexical — check symlink targets with filepath.EvalSymlinks for adversarial input","Route all file access inside the workspace through safeWorkspacePath instead of filepath.Join","Log rejected traversal attempts for security review"],"tags":["go","path-traversal","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}