T09 · Insecure Skill Coding Practices
- Location
index.js:47- Finding
Shell Command Injection Through Unvalidated Search Options
- Content
View full analysis
Vulnerability Details
File Location:
index.js, lines 47–64
Vulnerability Type: OS command injection
Risk Level: HighVulnerable Code
javascript // Build command const args = [ `"${query}"`, `--limit ${limit}`, `--year-min ${yearMin}`, `--sources ${sources.join(',')}`, deduplicate ? '--deduplicate' : '', refresh ? '--refresh' : '', ].filter(Boolean).join(' '); const command = `python "${SEARCH_SCRIPT}" ${args}`; const { stdout, stderr } = await execAsync(command, { cwd: SKILL_DIR, timeout: 300000, // 5 minutes maxBuffer: 10 * 1024 * 1024, });Technical Analysis
The function constructs a command string by directly interpolating the caller-controlled
query,limit,yearMin, andsourcesvalues. It then passes that string tochild_process.exec, which executes it through a system shell.Wrapping
queryin double quotes does not make it safe. Shell substitutions such as$(command)remain active inside double quotes, and embedded quotation marks can terminate the intended argument. The other interpolated fields are not quoted at all. The JSDoc type declarations do not provide runtime validation, so a caller can supply strings or specially crafted array elements despite the documented types.Attack Path
- An attacker gains control over any options passed to the exported
search()function, such as through an Agent invocation or an application endpoint that exposes the Skill. - The attacker submits a query containing shell syntax, for example a command substitution expression.
search()interpolates the payload into thecommandstring without shell escaping.execAsync()invokes the system shell.- The shell evaluates the injected expression before or while launching
search.py. - The injected operating-system command executes with the same identity and permissions as the Node.js Agent process.
The same path can be exp ...[truncated 580 chars]
- An attacker gains control over any options passed to the exported
- Remediation
View remediation
Remediation Suggestions
Replace
child_process.execwithexecFileorspawnand provide arguments as an array so that no shell parses caller-controlled values.javascript const { execFile } = require('child_process'); const { promisify } = require('util'); const execFileAsync = promisify(execFile); const allowedSources = new Set([ 'openalex', 'semantic_scholar', 'arxiv', ]); if (typeof query !== 'string' || query.length === 0 || query.length > 1000) { throw new TypeError('query must be a non-empty string of at most 1000 characters'); } if (!Number.isInteger(limit) || limit < 1 || limit > 50) { throw new RangeError('limit must be an integer between 1 and 50'); } if (!Number.isInteger(yearMin) || yearMin < 1000 || yearMin > 9999) { throw new RangeError('yearMin must be a four-digit integer'); } if ( !Array.isArray(sources) || sources.length === 0 || sources.some(source => !allowedSources.has(source)) ) { throw new TypeError('sources contains an unsupported source'); } const args = [ SEARCH_SCRIPT, query, '--limit', String(limit), '--year-min', String(yearMin), '--sources', sources.join(','), ]; if (deduplicate) args.push('--deduplicate'); else args.push('--no-deduplicate'); if (refresh) args.push('--refresh'); const { stdout, stderr } = await execFileAsync('python3', args, { cwd: SKILL_DIR, timeout: 300000, maxBuffer: 10 * 1024 * 1024, });In addition:
- Do not attempt to address this issue only through manual shell escaping; avoiding the shell is safer.
- Validate every exported function argument at runtime.
- Enforce the configured maximum result limit.
- Run the Skill under a dedicated, least-privileged operating-system account.
- Add regression tests using shell metacharacters, embedded quotes, command substitutions, and malformed
sourcesarrays.
