T05 · Unauthorized Access and Privilege Escalation
- Location
plugin/lib.ts:128- Finding
Workspace Escape Through Unvalidated File Paths
- Content
View full analysis
Vulnerability Details
File Location:
plugin/lib.ts:128-168
Vulnerability Type: Path traversal and workspace authorization bypass
Risk Level: MediumThe
fileparameter exposed by the TODO and note creation tools is documented as relative to the configured workspace, but the implementation does not enforce that boundary.Vulnerable Code
ts export function buildAddTodoArgs( cfg: OrgCliConfig, params: { title: string; scheduled?: string; deadline?: string; file?: string; }, ): string[] { const filePath = join(cfg.dir, params.file ?? cfg.inboxFile); const args = [ "add", filePath, params.title, "--todo", "TODO", "--db", cfg.db, "-f", "json", ]; if (params.scheduled) { args.push("--scheduled", params.scheduled); } if (params.deadline) { args.push("--deadline", params.deadline); } return args; } export function buildAddNoteArgs( cfg: OrgCliConfig, params: { text: string; file?: string; }, ): string[] { const filePath = join(cfg.dir, params.file ?? cfg.inboxFile); return ["add", filePath, params.text, "--db", cfg.db, "-f", "json"]; }These argument builders are reachable from the
org_add_todoandorg_add_notetools inplugin/index.ts:211-229andplugin/index.ts:257-274.Technical Analysis
join(cfg.dir, userControlledFile)normalizes path traversal components but does not verify that the resulting path remains insidecfg.dir. For example, with a workspace of/home/user/org, a value such as../../tmp/outside.orgresolves outside the declared workspace.The normalized path is subsequently passed to the configured
orgexecutable throughexecFile. The use ofexecFileprevents shell metacharacter expansion and therefore avoids direct shell injection, but it does not provide filesystem confinement or path authorizat ...[truncated 1464 chars]- Remediation
View remediation
Remediation Suggestions
-
Resolve and validate every destination against the canonical workspace root before invoking
org:ts import { isAbsolute, relative, resolve } from "node:path"; function resolveWorkspaceFile(workspace: string, input: string): string { if (isAbsolute(input)) { throw new Error("Absolute file paths are not permitted"); } const root = resolve(workspace); const candidate = resolve(root, input); const rel = relative(root, candidate); if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) { throw new Error("File path must remain inside the org workspace"); } return candidate; } -
Use this validation in both
buildAddTodoArgsandbuildAddNoteArgs, including when the destination comes fromcfg.inboxFile. -
If only top-level org filenames are required, apply a stricter allowlist: reject directory separators and accept only filenames matching a pattern such as
^[A-Za-z0-9._-]+\.org$. -
Where nested workspace files are supported, account for symbolic-link escapes by resolving the nearest existing parent with
realpath()and confirming it remains under the canonical workspace directory. -
Add tests covering:
../outside.org../../outside.org- absolute paths
- nested valid paths
- configured
inboxFiletraversal - symbolic links pointing outside the workspace
-
Reject invalid paths before spawning the external executable and return a structured authorization error to the tool caller.
-
