T09 · Insecure Skill Coding Practices
- Location
index.js:88- Finding
OS Command Injection Through Untrusted Project Creation Options
- Content
View full analysis
Vulnerability Details
File Location:
index.js, lines 88–111
Vulnerability Type: OS command injection through shell command construction
Risk Level: HighVulnerable Code
javascript async function createProject(projectType, dotnetVersion, outputDir) { const templateMap = { 'wpf': 'wpf', 'console': 'console', 'webapi': 'webapi', 'classlib': 'classlib', 'worker': 'worker', }; const template = templateMap[projectType] || 'console'; const targetDir = outputDir || path.join(process.cwd(), `CSharpApp-${Date.now()}`); console.log(`📁 Creating ${projectType} project at: ${targetDir}`); // Check if dotnet CLI is available try { await execAsync('dotnet --version'); } catch { throw new Error('.NET SDK not found. Please install .NET SDK from https://dotnet.microsoft.com/download'); } // Create project using dotnet CLI const command = `dotnet new ${template} -f net${dotnetVersion} -o "${targetDir}"`; await execAsync(command);Technical Analysis
The exported
createProject()function constructs a shell command by directly interpolating the caller-controlleddotnetVersionandoutputDirvalues. The resulting string is passed toexecAsync(), which is a promisified version ofchild_process.execand therefore executes the command through the operating-system shell.The
dotnetVersionvalue is not quoted or validated. An attacker can append shell metacharacters and an additional command. AlthoughoutputDiris enclosed in double quotes, an attacker can supply a double quote to terminate the argument and then inject shell syntax. Quoting without escaping or strict validation does not provide a security boundary.The
projectTypevalue is effectively allowlisted throughtemplateMapand falls back toconsole, so it is not the direct injection source. The vulnerable sources aredotnetVersionandoutputDir.At
...[truncated 1707 chars]
- Remediation
View remediation
Remediation Suggestions
-
Replace
child_process.execwithchild_process.execFileorchild_process.spawnand supply each argument separately so no shell parses caller-controlled values:javascript const { execFile } = require('child_process'); const { promisify } = require('util'); const execFileAsync = promisify(execFile); await execFileAsync('dotnet', ['--version']); await execFileAsync('dotnet', [ 'new', template, '-f', `net${validatedVersion}`, '-o', targetDir, ]); -
Strictly allowlist supported .NET versions instead of accepting arbitrary strings. If flexible versions are required, enforce an exact format such as:
javascript if (!/^\d+\.\d+$/.test(dotnetVersion)) { throw new Error('Unsupported .NET version'); }An explicit set such as
new Set(['6.0', '7.0', '8.0'])is preferable when supported versions are known. -
Resolve and normalize
outputDir, reject NUL characters, and constrain the destination to an approved workspace when callers are not fully trusted:javascript const workspace = path.resolve(process.cwd()); const targetDir = path.resolve(outputDir || path.join( workspace, `CSharpApp-${Date.now()}` )); if (targetDir !== workspace && !targetDir.startsWith(workspace + path.sep)) { throw new Error('Output directory must remain inside the workspace'); } -
Execute the skill with least privilege and avoid exposing sensitive credentials to the process environment.
-
Add automated tests containing shell metacharacters, quotes, command substitutions, newlines, and path traversal sequences to verify that inputs are rejected or treated strictly as literal arguments.
-
