T09 · Insecure Skill Coding Practices
Warning
- Location
- index.ts:52
- Finding
- Subagent Status Confusion Caused by Reusable Labels<![CDATA[ ## Vulnerability Details **File Location**: `index.ts`, lines 52–67 **Vulnerability Type**: Improper subagent identity validation **Risk Level**: Medium ### Vulnerable Code ```typescript const status = await subagents({ action: 'list', recentMinutes: 60, }) const agent = status.recent?.find((a: any) => a.label === label && a.status === 'done' ) if (agent) { console.log(`✅ ${label} completed`) return agent } const failed = status.recent?.find((a: any) => a.label === label && a.status === 'failed' ) ``` ### Technical Analysis `sessions_spawn` returns a unique `childSessionKey`, but the pipeline does not use that identifier while waiting for completion. Instead, `waitForAgent` searches every subagent reported within the preceding 60 minutes and accepts the first session whose reusable label and status match. The pipeline repeatedly uses predictable labels such as `planner`, `pm`, `coder`, `tester`, `fixer`, and `reporter`. Consequently, a stale session or a concurrently running pipeline with the same label can satisfy the lookup. The waiting code may then treat an unrelated session as the stage it just spawned. This is a time-of-check and identity-confusion flaw. The code verifies a mutable, non-unique display label rather than the immutable identifier returned when the relevant subagent was created. ### Attack Path 1. An attacker with the ability to start subagents in the same observable environment, or another concurrent pipeline invocation, starts an agent using a predictable label such as `planner`. 2. That unrelated agent reaches the `done` or `failed` state while remaining within the `recentMinutes: 60` query window. 3. A victim invokes `spawnDevTeam`, which starts its own agent with the same label. 4. `waitForAgent` lists recent subagents and searches only by label and status. 5. The unrelated agent is selected before the victim's newly spawned agent has completed. 6. The pipeline incorrectly advances to the next stage or t ...[truncated 1081 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Capture the `childSessionKey` returned by `sessions_spawn` and pass it directly to `waitForAgent`. 2. Match status records using that immutable session key rather than the human-readable label. 3. If the status API cannot filter by session key, generate invocation-scoped, cryptographically unpredictable labels and verify all available identity fields. 4. Avoid searching a broad 60-minute global session window when waiting for one known child session. 5. Record the expected session identifier, creation time, pipeline invocation ID, and role, then validate all of them before accepting a terminal status. 6. Add concurrency tests that launch multiple pipelines with identical role names and confirm that each pipeline waits only for its own children. 7. Reject ambiguous results instead of selecting the first matching label. A hardened design would resemble: ```typescript async function spawnAgent(agent: DevTeamAgent) { return sessions_spawn({ label: agent.label, task: agent.task, model: agent.model, mode: 'run', runtime: 'subagent', }) } async function waitForAgent(childSessionKey: string, timeout: number) { const startTime = Date.now() while (Date.now() - startTime < timeout) { const status = await subagents({ action: 'list', recentMinutes: 60, }) const agent = status.recent?.find( (candidate: any) => candidate.childSessionKey === childSessionKey ) if (agent?.status === 'done') { return agent } if (agent?.status === 'failed') { throw new Error(`Subagent ${childSessionKey} failed`) } await new Promise(resolve => setTimeout(resolve, 30000)) } throw new Error(`Subagent ${childSessionKey} timed out`) } ``` Each pipeline stage should call `waitForAgent` with the exact identifier returned by its corresponding `spawnAgent` call. ]]>
