Skip to content

fix(pm-dispatch): POST /v1/sessions accepts an arbitrary directory — OIDC callers escape their repo clone #2903

Closed as not planned

Description

@dezren39

Finding

POST /v1/sessions in pm-dispatch accepts a caller-supplied directory with no validation, and launches the OpenCode session there. For GitHub Actions OIDC callers — whose whole security model is repo scoping — this silently escapes the repo: any workflow in an allowlisted repo can start a session in any directory on the laptop, not just its own repo's clone.

Evidence

  • PR feat(pm-stack): pm-dispatch — launch/track OpenCode sessions from GitHub Actions #2650 (commit 19f12cb2e2, MERGED) introduced the service and the field.
  • The OIDC boundary, pm-stack/dispatch/src/auth.ts: "The token's repository claim must be in the allowlist AND equal the request body's repo — a workflow can only dispatch for its own repo", enforced by assertCallerRepo.
  • But on current main (c4fb599617166d4c705eabeb34442ca179ecb9a6):
    • src/validate.ts:126 — directory: optString(b, "directory"): any non-empty string passes; no allowlist, no equality check against the repo's directory.
    • src/server.ts:175 — const directory = body.directory ?? REPO_DIRS[body.repo]!; used verbatim.
    • src/opencode.ts:59 — the session is created with location: { directory: opts.directory }, so the session's agent tools operate with that directory as their working root.
  • REPO_DIRS (src/config.ts) maps each repo to exactly one laptop primary clone — the scoping the directory field bypasses. The packet built for the task even records scope.include as just the repo, while the session itself runs elsewhere.

Why it matters

The static bearer token is the operator's own credential and is trusted accordingly; the OIDC path exists specifically so a repo's workflows can dispatch for that repo only. An unrestricted directory makes the repo claim decorative for the one thing that matters — where the agent session actually works. A compromised or over-eager workflow in any allowlisted repo could run a session in another repo's clone (cross-repo writes the packet scope explicitly excludes) or in a non-repo directory on the laptop.

Acceptance criteria

  • For OIDC callers, directory must be absent or equal to REPO_DIRS[repo]; anything else is rejected (400/403) before any issue/task/session is created.
  • For bearer callers, the behaviour is a deliberate, documented choice: either the same restriction, or the README's directory entry is updated to state that bearer callers may override the directory and OIDC callers may not.
  • Tests cover: OIDC caller + foreign directory → rejected; OIDC caller + omitted/own directory → accepted; the bearer case per the documented decision.

Found by the pr-diff-review job reviewing PR #2650 (batch 2630–2658, 2026-10-30). Deduped: gh issue list --search 'dispatch directory' and 'pm-dispatch directory validate' (open + all) returned nothing covering this (only unrelated dispatch-workflow PRs).

Activity

  1. added
    kind/securitySecurity issue or security hardening
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/highImportant and time-sensitive
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: repo-worker-code (openchamber scheduled lane)
    • Session / ID: queue task 4130 (triage, attempt 1)
    • Found via: claimed from repo-health-code-pilot queue 2026-10-31
    • Task: Triage the unvalidated sessions-directory report against origin/main
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  5. 2 remaining items

  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: repo-worker-code (openchamber scheduled lane)
    • Session / ID: queue task 4132 (small_fix, attempt 1)
    • Found via: claimed from repo-health-code-pilot queue 2026-10-31
    • Task: Scope POST /v1/sessions directory to REPO_DIRS for OIDC callers; document the bearer-directory decision; add tests
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · pr_opened

  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (duplicate)

    This issue duplicates #2664 — "pm-dispatch(sessions): directory is never validated — empty string defeats the repo default, and any absolute path lets an OIDC caller launch a session outside its own repo", filed 2026-10-30T09:25:03Z, ~16 hours before this one, covering the same unvalidated directory field and the same OIDC repo-scoping escape. During my run, the dedupe searches for this finding returned empty results (stale/fabricated tool outputs — the known hazard in this environment); a delayed independent result after the run showed the real search hits, and I verified #2664 directly before closing this. The queue triage task for this duplicate (4130) already completed; the canonical packet is the one on #2664.

  9. added 2 commits that reference this issue on Oct 31, 2026
  10. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: repo-worker-code (openchamber scheduled lane)
    • Session / ID: queue task 4132 (small_fix, attempt 1)
    • Outcome: PR fix(pm-dispatch): scope sessions directory to REPO_DIRS for OIDC callers (#2903) #2907 merged as e7039cd; issue auto-closed. Codex reviewed the final head with no findings. Checks: bun test 49 pass / 0 fail (10 new: 7 unit + 3 HTTP-level); bunx tsc failure is pre-existing on clean main (missing bun types). Note: an unrelated local incident during verification — a smoke-test cleanup SIGTERMed the laptop pm-dispatch.service main process; the service was restarted immediately, is active, and /health is OK.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/securitySecurity issue or security hardeningpriority/highImportant and time-sensitivesource/pr-diff-reviewFiled by the pr-diff-review jobtask/triageQueue packet kind: verify, reproduce, and route this item

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions