CVSS Vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8 High)
CWE: CWE-22 Path Traversal (arbitrary file write)
Summary
When Plandex applies a plan's file changes, the destination path for each file comes from the model's streamed output and is joined to the project root with no containment check. A path containing parent-directory segments escapes the project directory, so a plan whose content includes a file with a traversal path writes attacker-controlled content to an arbitrary location on the host. The apply confirmation prompt only states how many files will be applied and does not show or constrain the paths, and full-auto / --auto-apply mode applies with no prompt at all. Confirmed by calling the real ApplyFiles function: a file was written outside the configured project root.
Details
The destination is built in app/cli/lib/apply.go:
// ApplyFiles (~line 622), per file:
dstPath := filepath.Join(fs.ProjectRoot, path) // ~line 638
...
os.MkdirAll(filepath.Dir(dstPath), 0755) // ~line 679 (creates parents)
os.WriteFile(dstPath, []byte(content), 0644) // ~line 687
path originates from the model stream parser app/server/model/plan/tell_stream_processor.go, where openingTagRegex captures <PlandexBlock ... path="(.+?)">; the (.+?) capture accepts ../. There is no filepath.Rel / strings.HasPrefix(ProjectRoot) / containment check anywhere between that source and the os.WriteFile sink (grep-confirmed). filepath.Join("/home/user/project", "../../../etc/cron.d/x") resolves to /etc/cron.d/x. Because os.MkdirAll creates intermediate directories, the write succeeds for new paths as well.
The trust boundary is that applied changes land inside the project directory. The model output is influenced by untrusted inputs (poisoned repository files, attacker-controlled context loaded into the plan, or indirect prompt injection), so an attacker who can influence the model's response can write outside the project, for example to ~/.bashrc, a shell rc, or a cron file, yielding code execution.
PoC
Impact
An attacker who can influence the model's streamed plan (via a poisoned repo file, attacker-controlled context, or prompt injection) writes attacker-controlled content to an arbitrary path on the developer's machine, escaping the project directory. Writing to shell startup files or cron yields code execution. The apply confirmation does not reveal the destination paths, and --auto-apply / full-auto applies with no prompt.
Remediation
After computing dstPath = filepath.Join(ProjectRoot, path), enforce containment: rel, err := filepath.Rel(ProjectRoot, dstPath); reject if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || filepath.IsAbs(rel). Reject absolute paths and .. segments in the model-supplied path at parse time as well, and show the resolved destination paths in the apply confirmation.
Version: 2.2.1
Package: plandex (CLI)
CVSS Vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8 High)
CWE: CWE-22 Path Traversal (arbitrary file write)
Summary
When Plandex applies a plan's file changes, the destination path for each file comes from the model's streamed output and is joined to the project root with no containment check. A path containing parent-directory segments escapes the project directory, so a plan whose content includes a file with a traversal path writes attacker-controlled content to an arbitrary location on the host. The apply confirmation prompt only states how many files will be applied and does not show or constrain the paths, and full-auto /
--auto-applymode applies with no prompt at all. Confirmed by calling the realApplyFilesfunction: a file was written outside the configured project root.Details
The destination is built in
app/cli/lib/apply.go:pathoriginates from the model stream parserapp/server/model/plan/tell_stream_processor.go, whereopeningTagRegexcaptures<PlandexBlock ... path="(.+?)">; the(.+?)capture accepts../. There is nofilepath.Rel/strings.HasPrefix(ProjectRoot)/ containment check anywhere between that source and theos.WriteFilesink (grep-confirmed).filepath.Join("/home/user/project", "../../../etc/cron.d/x")resolves to/etc/cron.d/x. Becauseos.MkdirAllcreates intermediate directories, the write succeeds for new paths as well.The trust boundary is that applied changes land inside the project directory. The model output is influenced by untrusted inputs (poisoned repository files, attacker-controlled context loaded into the plan, or indirect prompt injection), so an attacker who can influence the model's response can write outside the project, for example to
~/.bashrc, a shell rc, or a cron file, yielding code execution.PoC
Impact
An attacker who can influence the model's streamed plan (via a poisoned repo file, attacker-controlled context, or prompt injection) writes attacker-controlled content to an arbitrary path on the developer's machine, escaping the project directory. Writing to shell startup files or cron yields code execution. The apply confirmation does not reveal the destination paths, and
--auto-apply/ full-auto applies with no prompt.Remediation
After computing
dstPath = filepath.Join(ProjectRoot, path), enforce containment:rel, err := filepath.Rel(ProjectRoot, dstPath); reject if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || filepath.IsAbs(rel). Reject absolute paths and..segments in the model-suppliedpathat parse time as well, and show the resolved destination paths in the apply confirmation.Version: 2.2.1
Package: plandex (CLI)