Files
orca/src/main/daemon/pty-session-id.ts
T
2026-06-17 23:28:54 -07:00

68 lines
3.0 KiB
TypeScript

import { randomUUID } from 'crypto'
import { isAbsolute, join, relative, resolve, sep } from 'path'
import { PTY_SESSION_ID_SEPARATOR } from '../../shared/pty-session-id-format'
// Why: re-exported here so main-side callers can keep importing
// `parsePtySessionId` from this module (next to `mintPtySessionId`). The
// implementation lives in `src/shared/` because the renderer-side merge
// helper also needs it and cannot import node-only modules.
export { parsePtySessionId } from '../../shared/pty-session-id-format'
/**
* Session IDs use the format `${worktreeId}@@${shortUuid}` so that
* DaemonPtyAdapter.reconcileOnStartup (see daemon-pty-adapter.ts) can
* derive the owning worktree by splitting on the @@ separator.
*
* Both pty.ts (host-daemon spawn path) and DaemonPtyAdapter.doSpawn
* (fallback when opts.sessionId is absent) must use this helper — a
* drifted format would break cold-restore mapping and legacy Pi overlay
* cleanup keying.
*/
export function mintPtySessionId(worktreeId?: string): string {
return worktreeId
? `${worktreeId}${PTY_SESSION_ID_SEPARATOR}${randomUUID().slice(0, 8)}`
: randomUUID()
}
/**
* Why: `effectiveSessionId` is used as a filesystem key for provider hook
* state and legacy Pi overlay cleanup under app.getPath('userData'). The
* security property we want is containment: derived paths must be strictly
* inside the userData root so a crafted IPC payload (args.sessionId or
* args.worktreeId forwarded from the renderer) cannot make us write outside
* userData.
*
* Callers pass `app.getPath('userData')` as `userDataPath`. Any
* subpath inside userData is acceptable as a filesystem key since callers use
* deeper roots under userData — enforcing "id cannot escape userData" is a
* superset of the per-feature containment checks.
*
* Note: real worktreeIds are `${repo.id}::${absolutePath}` so minted
* session ids contain `/` on POSIX and `\` on Windows. Rejecting
* those chars outright would break every real daemon spawn with a
* worktree. We instead compute `join(userDataPath, id)` and assert
* the normalized result is a strict subpath — this rejects `..`
* sequences, absolute-path injection, and NUL truncation attacks
* without false positives on legitimate path-shaped ids.
*/
export function isSafePtySessionId(id: string, userDataPath: string): boolean {
if (id.length === 0 || id.length > 512) {
return false
}
if (id.includes('\0')) {
return false
}
const resolvedRoot = resolve(userDataPath)
const resolvedTarget = resolve(join(userDataPath, id))
const rel = relative(resolvedRoot, resolvedTarget)
// Why: `path.relative` can return an absolute path when the target lives
// on a different drive or under a UNC share on Windows (e.g. relative
// from C:\userdata to D:\evil yields "D:\evil", which does NOT start with
// ".."). Reject any absolute result — a legitimate subpath under
// userData always produces a relative result on every platform.
if (rel === '' || isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) {
return false
}
return true
}