{"record":{"id":"96389447b511ad6f","repo":"Tencent/WeKnora","slug":"sandbox-workspace-write-path-q-is-outside-s","errorCode":null,"errorMessage":"sandbox: workspace write path %q is outside %s","messagePattern":"sandbox: workspace write path %q is outside (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/session_manager.go","lineNumber":1028,"sourceCode":"\treturn \"\", fmt.Errorf(\n\t\t\"sandbox: session input path %q is outside %s\",\n\t\tfilePath, SessionInputRoot,\n\t)\n}\n\n// cleanSessionWorkspaceWritePath keeps model-authored writes inside the\n// session workspace and out of the attachment tree. Validation is lexical\n// (path.Clean plus prefix checks), matching cleanSessionWorkDir.\nfunc cleanSessionWorkspaceWritePath(filePath string) (string, error) {\n\tclean := path.Clean(strings.TrimSpace(filePath))\n\tif !path.IsAbs(clean) || clean == \".\" || clean == \"/\" {\n\t\treturn \"\", fmt.Errorf(\"sandbox: workspace write path %q must be an absolute file path\", filePath)\n\t}\n\tif clean == SessionWorkspaceRoot || clean == SessionOutputRoot || clean == SessionInputRoot {\n\t\treturn \"\", fmt.Errorf(\"sandbox: workspace write path %q is a directory, not a file\", filePath)\n\t}\n\tif !strings.HasPrefix(clean, SessionWorkspaceRoot+\"/\") {\n\t\treturn \"\", fmt.Errorf(\"sandbox: workspace write path %q is outside %s\", filePath, SessionWorkspaceRoot)\n\t}\n\tif strings.HasPrefix(clean, SessionInputRoot+\"/\") {\n\t\treturn \"\", fmt.Errorf(\"sandbox: session input %s is read-only\", SessionInputRoot)\n\t}\n\treturn clean, nil\n}\n\n// cleanSessionWorkDir keeps shell_exec inside directories we are willing to let\n// an agent work in. Ordinary sessions get /workspace only.\n//\n// Validation is lexical (path.Clean plus prefix checks): a symlink under an\n// allowed root that resolves elsewhere at execution time is not detected and\n// that is intentional. The only caller that passes allowSkillsRoot also passes\n// AsRoot and runs arbitrary install shell commands, so a symlink would grant\n// nothing those commands cannot already reach via cd or absolute paths. For\n// ordinary sessions the allowlist is unchanged and its lexical nature is\n// pre-existing. The allowlist stops casual wandering and makes intent\n// auditable; the real isolation boundary is the remote sandbox itself.","sourceCodeStart":1010,"sourceCodeEnd":1046,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/session_manager.go#L1010-L1046","documentation":"WriteSessionWorkspaceFile sanitizes the requested path through cleanSessionWorkspaceWritePath before writing. This error is returned when the cleaned path is absolute and a real file path but does not live under the session workspace root /workspace (e.g. /etc/passwd, /tmp/x, /workspace-evil/file). The sandbox deliberately restricts writes to /workspace to keep agent output contained.","triggerScenarios":"Calling WriteSessionWorkspaceFile (or the test helper path) with a file path whose cleaned value is not prefixed by \"/workspace/\" — e.g. absolute paths to other directories, or paths that clean to something outside /workspace such as \"/workspace/../etc/hosts\".","commonSituations":"Hardcoding an OS temp path or a host-relative path instead of a /workspace-relative one; constructing paths with \"..\" segments that escape the workspace; migrating code that previously wrote to arbitrary directories.","solutions":["Rewrite the path to be under /workspace, e.g. \"/workspace/output/report.txt\".","Call filepath.Clean/Join yourself before calling and verify strings.HasPrefix(cleaned, \"/workspace/\") to fail fast with a clearer message.","If the data is an input attachment, read it via the input-path API instead of attempting a write into /workspace/input."],"exampleFix":"// before\nerr := mgr.WriteSessionWorkspaceFile(ctx, session, \"/tmp/report.txt\", data)\n// after\nerr := mgr.WriteSessionWorkspaceFile(ctx, session, \"/workspace/output/report.txt\", data)","handlingStrategy":"validation","validationCode":"func isWorkspaceWritePath(p string) bool {\n    c := filepath.Clean(p)\n    return filepath.IsAbs(c) && strings.HasPrefix(c, \"/workspace/\") &&\n        c != \"/workspace/input\" && !strings.HasPrefix(c, \"/workspace/input/\")\n}\nif !isWorkspaceWritePath(p) { /* fix path before calling */ }","typeGuard":"func safeWorkspacePath(p string) (string, bool) {\n    c := filepath.Clean(p)\n    if !filepath.IsAbs(c) || !strings.HasPrefix(c, \"/workspace/\") {\n        return \"\", false\n    }\n    return c, true\n}","tryCatchPattern":"out, err := mgr.WriteSessionWorkspaceFile(ctx, s, path, data)\nif err != nil {\n    return fmt.Errorf(\"write workspace file %q: %w\", path, err)\n}","preventionTips":["Always build paths with filepath.Join(\"/workspace\", rel) instead of string concatenation.","Refuse user-supplied relative paths that contain \"..\" before joining.","Keep a unit test asserting all write targets start with /workspace/."],"tags":["sandbox","path-validation","security"],"backgroundTag":"path-outside-sandbox-root","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}