Skip to content

fix(daemon): reject '.', '..' and empty session names so session artifacts stay under sessions/ - #1815

Merged
thymikee merged 1 commit into
mainfrom
fix/session-name-relative-dir-escape
Aug 18, 2026
Merged

fix(daemon): reject '.', '..' and empty session names so session artifacts stay under sessions/#1815
thymikee merged 1 commit into
mainfrom
fix/session-name-relative-dir-escape

Conversation

@thymikee

Copy link
Copy Markdown
Member

Problem

`safeSessionName` only rewrites characters outside `[a-zA-Z0-9._-]`, so the names `.` and `..` survive unchanged. `SessionStore.resolveSessionDir` then does `path.join(sessionsDir, '..')` → the daemon state dir, and `app.log` / `runner.log` / `requests/*.ndjson` are written there. For a remote daemon this lets a caller steer daemon-side artifact writes outside the sessions tree with `--session ..`.

Fix — at the owning interface

`SessionStore.resolveSessionDir` is the one place a session name becomes a directory (AGENTS.md: session artifact paths come from `session-store.ts`), so the invariant "every session dir lies beneath `sessionsDir`" is enforced there: an unsafe name (`.`, `..`, empty after `safeSessionName`) throws `INVALID_ARGS`.

Why not (only) at `scopeRequestSession`: `createRequestExecutionScope` calls `resolveSessionDir` first thing after resolving the effective name — before any artifact path is used — so the store-level check is the admission-time rejection, and the request finalizes as an `INVALID_ARGS` response like the existing tenant-id rejection does. Every other caller (`ensureSessionDir`, app-log/event-log/record paths, inventory, doctor, teardown) receives an already admitted name, so nothing else changes. Session state is in-memory only, so no on-disk record can carry an unsafe name across the upgrade.

Checked for existing behavior depending on relative names: none — no code, test, or docs use `--session .` / `..`; `defaultTracePath`, the `.ad` writer, metro-session hints and claim/recovery all build file names under a fixed dir (a `..-.trace.log` file is harmless).

Overlap with #1814

`isSafeSessionSegment` is the same predicate #1814 adds for its request-diagnostics route (which deliberately left `resolveSessionDir` alone). Whichever lands second takes a one-hunk merge in `session-paths.ts`.

Tests (proven red first)

  • `session-store.test.ts`: `resolveSessionDir` rejects `.`, `..`, `''` with `INVALID_ARGS`; a normal name stays under `sessionsDir`. Pre-fix: "Missing expected exception".
  • `request-execution-scope.test.ts`: a request with `session: '..'` is rejected and nothing lands in the state dir. Pre-fix: the scope resolved with `runnerLogPath: /runner.log`.

`pnpm check:affected --run` green (336 files / 2834 tests, format, layering, fallow, build).

…onDir

safeSessionName only rewrites characters outside [a-zA-Z0-9._-], so the names
'.' and '..' survive unchanged and path.join resolves them to the sessions dir
itself or its parent, the daemon state dir. A remote caller's --session ..
would then land app.log / runner.log / requests/*.ndjson outside the sessions
tree.

SessionStore.resolveSessionDir is the one place a session name becomes a
directory (AGENTS.md: session artifact paths come from session-store), so it now
refuses such a name with INVALID_ARGS. Every request goes through it first
thing in createRequestExecutionScope, before any artifact path is used, so this
is also the admission-time rejection; every other caller passes an already
admitted name.

isSafeSessionSegment mirrors the predicate PR #1814 adds for its
request-diagnostics route; whichever lands second takes the trivial merge.

Regression tests were proven red against the pre-fix code: resolveSessionDir
returned the sessions dir / state dir for '.', '..', '' and the request scope
resolved runnerLogPath to <stateDir>/runner.log.
@github-actions

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.27 MB 2.27 MB +192 B
JS gzip 745.8 kB 745.8 kB +67 B
npm tarball 866.4 kB 866.4 kB +63 B
npm unpacked 3.02 MB 3.02 MB +192 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 33.5 ms 28.2 ms -5.3 ms
CLI --help 71.3 ms 66.5 ms -4.8 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/screenshot-result.js +192 B +67 B

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 18, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Clean review at bb5ff957: unsafe empty, . and .. session segments are rejected at the owning SessionStore directory boundary before request or runner artifacts can form. The production-scope and path-containment regressions are meaningful, and no device evidence is needed for this filesystem-validation change. All completed checks pass; Coverage and two Smoke lanes remain pending. Code review is ready-for-human.

@thymikee
thymikee merged commit 20458cb into main Aug 18, 2026
31 checks passed
@thymikee
thymikee deleted the fix/session-name-relative-dir-escape branch August 18, 2026 09:57
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-18 09:57 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant