Skip to content

Path traversal in apply-changes writes files outside the project directory (model-controlled path) #352

Description

@geo-chen

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions