Skip to content

PATCH /api/projects/:slug: repo is mutable and unvalidated — relocates workingDir to a nonexistent dir and bricks the project #718

Description

@edspencer

Summary

PATCH /api/projects/:slug accepts a repo value with no validation and lets it relocate the project's workingDir and contentDir to a directory that does not exist. The project is left bricked — every turn hangs for 60s and fails — and any pre-existing chats, which are keyed on the old cwd, are stranded.

path and managed are already re-asserted from current precisely to prevent this. repo moves the cwd through the identical workingDirFor() code path and gets none of that protection.

Reproduction (v0.65.0, plain notebook project)

curl -sX POST /api/projects -H 'content-type: application/json' -d '{"name":"Demo"}'
# workingDir: <dataDir>/projects/demo

curl -sX PATCH /api/projects/demo -H 'content-type: application/json' \
     -d '{"repo":"not a url at all ;rm -rf /"}'
# -> 200 OK

After:

workingDir: <dataDir>/projects/demo/not-a-url-at-all--rm--rf   # does NOT exist
contentDir: <dataDir>/projects/demo/not-a-url-at-all--rm--rf   # curated notes orphaned

No clone happens (that is promote()'s job). Every subsequent turn then fails:

CLI execution failed: Timeout waiting for new session file in
  …-not-a-url-at-all--rm--rf (waited 60000ms)
sessionId: null

Confirmed 3×. Reachable from the ordinary Settings-pane PATCH route, not just curl.

Root cause

  • create() validates with isValidRepoUrl()projects.ts:532. promote() validates too — projects.ts:769. update() does neither.

  • update() re-asserts path and managed from current (projects.ts:875-877) with a comment that states the reasoning exactly:

    one must not be given a path by a stray key, which would silently hand its keeper a cwd somewhere else on the box that never went through validatePath

    repo is the third field that determines workingDirFor() and is left mutable and unvalidated.

Suggested fix

Either re-assert repo from current the way path/managed are (changing a backing store is a migration, not a PATCH — cf. #708), or run isValidRepoUrl() in update() and refuse a change that would move workingDir for a project that already has chats.

Test gap

packages/server/test/integration/projects-crud.test.ts covers PATCH but never asserts that the backing fields are immutable. A test asserting "PATCH cannot move workingDir" would cover path, managed and repo together.

Found during a v0.65.0 exploratory QA session; verified independently against a live instance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions