T09 · Insecure Skill Coding Practices
- Location
src/executor.js:164- Finding
Caller-Controlled Execution ID Enables Checkpoint Path Traversal
- Content
View full analysis
Vulnerability Details
File Location:
src/executor.js:31,src/executor.js:164-190
Vulnerability Type: Path traversal allowing unauthorized JSON file access and overwrite
Risk Level: HighVulnerable Code
javascript async execute(dag, context = {}, executionId = null) { executionId = executionId || `exec_${Date.now()}`; this.emit('execution-started', { executionId, dag: dag.id }); // Check checkpoint let state = await this.loadCheckpoint(executionId);javascript async saveCheckpoint(state) { try { await fs.mkdir(this.checkpointDir, { recursive: true }); const checkpointPath = path.join(this.checkpointDir, `${state.executionId}.json`); const checkpoint = { ...state, completedNodes: Array.from(state.completedNodes), failedNodes: Array.from(state.failedNodes), nodeResults: Array.from(state.nodeResults.entries()) }; await fs.writeFile(checkpointPath, JSON.stringify(checkpoint), 'utf-8'); } catch (error) { console.warn('Checkpoint save failed:', error); } } async loadCheckpoint(executionId) { try { const checkpointPath = path.join(this.checkpointDir, `${executionId}.json`); const content = await fs.readFile(checkpointPath, 'utf-8'); const checkpoint = JSON.parse(content); return { ...checkpoint, completedNodes: new Set(checkpoint.completedNodes), failedNodes: new Set(checkpoint.failedNodes), nodeResults: new Map(checkpoint.nodeResults) }; } catch { return null; } }Technical Analysis
The public execution API accepts an externally supplied
executionIdand passes it directly toloadCheckpoint(). The same identifier is subsequently used bysaveCheckpoint().Appending
.jsondoes not neutralize directory traversal sequences. For example, joining the default checkpoint directory with an execution ID such as../../data/targetresolves outside the intended checkpoint directory. No allowlist validation, ca ...[truncated 1679 chars]- Remediation
View remediation
Remediation Suggestions
- Do not accept filesystem identifiers directly from untrusted callers. Generate execution IDs internally using
crypto.randomUUID(). - If caller-supplied IDs are required, enforce a strict allowlist:
javascript const EXECUTION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/; function validateExecutionId(executionId) { if (!EXECUTION_ID_PATTERN.test(executionId)) { throw new Error('Invalid execution ID'); } }- Resolve and verify the resulting path before every read or write:
javascript const checkpointRoot = path.resolve(this.checkpointDir); const checkpointPath = path.resolve(checkpointRoot, `${executionId}.json`); if ( checkpointPath === checkpointRoot || !checkpointPath.startsWith(`${checkpointRoot}${path.sep}`) ) { throw new Error('Checkpoint path escapes the configured directory'); }- Reject absolute paths, path separators, traversal components, null bytes, and excessively long identifiers.
- Write checkpoints atomically through a temporary file in the validated directory, then rename it.
- Do not silently suppress security-relevant filesystem errors. Emit structured audit events without exposing sensitive contents.
- Add tests covering
../, absolute paths, platform-specific separators, encoded separators, and symlink-related escape attempts.
- Do not accept filesystem identifiers directly from untrusted callers. Generate execution IDs internally using
