Merge commit from fork

* Fix unauthenticated command injection (RCE) in Studio server

The `--studio` server (cli-tool/src/sandbox-server.js) bound to 0.0.0.0
with wildcard CORS and no authentication, and passed request-body fields
into spawn(..., { shell: true }). Because shell:true makes Node join the
argv array into a single `sh -c` string, the fields were parsed by the
shell, allowing OS command injection (RCE) via POST /api/execute (prompt)
and POST /api/install-agent (agentName), reachable from the LAN or a
malicious web page (wildcard CORS).

- Remove shell:true from all three spawns so arguments stay discrete argv
  entries; shell metacharacters can no longer inject commands.
- Validate agentName against ^[A-Za-z0-9._/-]+$ before use.
- Bind the server to 127.0.0.1 only (removes the LAN vector).
- Restrict CORS to the local UI origin and reject cross-origin requests
  (removes the drive-by browser vector).

Fixes GHSA-79wm-x847-7cvg.

Co-Authored-By: Claude Opus <noreply@anthropic.com>

* security: Windows-safe spawns + release bump on top of GHSA-79wm-x847-7cvg fix

Builds on @spartan8806's fix (advisory-fix-1). Adds the two pieces that fix
lacked:

- Resolve npx/claude via their .cmd shims on Windows (NPX_CMD/CLAUDE_CMD).
  Without shell:true, bare spawn('npx'/'claude') fails with ENOENT on win32,
  which would break agent install/local execution. This keeps the security fix
  cross-platform.
- Bump version to 1.29.4 (root + cli-tool package.json) and add the 1.29.4
  Security entry to CHANGELOG for the patched release.

Refs GHSA-79wm-x847-7cvg.

---------

Co-authored-by: spartan8806 <spartan8806@users.noreply.github.com>
Co-authored-by: Claude Opus <noreply@anthropic.com>
Co-authored-by: Dani <dan.avila7@gmail.com>
This commit is contained in:
spartan
2026-07-14 19:25:34 -04:00
committed by GitHub
co-authored by spartan8806 Claude Opus Dani
parent acdb589341
commit bc4618b072
4 changed files with 79 additions and 19 deletions
+20
View File
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.29.4] - 2026-07-13
### Security
- **Fix unauthenticated OS command injection / RCE in Claude Code Studio
(`--studio`)** — GHSA-79wm-x847-7cvg (CWE-78, CWE-306, CWE-352; CVSS 8.8).
Reported by @spartan8806. The studio server
(`cli-tool/src/sandbox-server.js`) bound to all interfaces, sent
`Access-Control-Allow-Origin: *`, required no auth, and passed request-body
fields into `child_process.spawn(..., { shell: true })`. Fixes:
- Removed `shell: true` from all task/agent spawns so arguments stay discrete
argv entries and shell metacharacters can no longer execute.
- Added a strict allowlist (`^[A-Za-z0-9._/-]+$`) for the `agentName`/`agent`
fields, rejected before reaching any child process.
- Bound the server to loopback (`127.0.0.1`) instead of `0.0.0.0`, removing
the LAN attack surface.
- Restricted CORS to the local Studio UI origin and rejected cross-origin
requests, closing the drive-by CSRF vector.
- Resolved `npx`/`claude` via their `.cmd` shims on Windows so removing
`shell: true` does not break agent installation on win32.
## [Unreleased]
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-code-templates",
"version": "1.28.13",
"version": "1.29.4",
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
"main": "src/index.js",
"bin": {
+57 -17
View File
@@ -9,6 +9,12 @@ const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3444;
// Platform-aware command names. Now that spawns run without `shell: true`,
// Windows needs the `.cmd` shim to resolve npm-installed executables (bare
// 'npx'/'claude' would fail with ENOENT on win32).
const NPX_CMD = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const CLAUDE_CMD = process.platform === 'win32' ? 'claude.cmd' : 'claude';
// Load .env file from current working directory (where user runs the command)
function loadEnvFile() {
const envPath = path.join(process.cwd(), '.env');
@@ -47,11 +53,26 @@ function loadEnvFile() {
// Load environment variables on startup
const hasApiKeys = loadEnvFile();
// Simple CORS middleware
// CORS middleware — restrict to the local Studio UI origin only.
// A wildcard (`*`) origin combined with the command-executing endpoints below
// lets any web page the developer visits drive requests into this server
// (drive-by RCE). Only allow the same-origin UI served from localhost:PORT.
const ALLOWED_ORIGINS = new Set([
`http://localhost:${PORT}`,
`http://127.0.0.1:${PORT}`,
]);
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Vary', 'Origin');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
// Reject cross-origin requests outright for the state-changing endpoints.
if (origin && !ALLOWED_ORIGINS.has(origin)) {
return res.status(403).json({ success: false, error: 'Cross-origin request rejected' });
}
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
@@ -199,22 +220,31 @@ app.get('/api/tasks', (req, res) => {
// API endpoint to install agent
app.post('/api/install-agent', async (req, res) => {
const { agentName } = req.body;
if (!agentName) {
return res.status(400).json({
success: false,
error: 'Agent name is required'
});
}
// SECURITY: agent names are `category/name` slugs. Reject anything else so a
// value like "x; rm -rf ~" can never reach the child process.
if (!/^[A-Za-z0-9._/-]+$/.test(agentName)) {
return res.status(400).json({
success: false,
error: 'Invalid agent name'
});
}
try {
console.log(chalk.blue('🔧 Installing agent:'), chalk.cyan(agentName));
// Use the CLI tool to install the agent
const child = spawn('npx', ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
// SECURITY: shell:false (default) keeps agentName as a single argv entry —
// no shell parses it, so metacharacters cannot inject commands.
const child = spawn(NPX_CMD, ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
stdio: ['pipe', 'pipe', 'pipe']
});
let output = [];
@@ -276,11 +306,17 @@ async function checkAndInstallAgent(agentName, task) {
task.output.push(`🔧 Agent ${agentName} not found locally. Installing...`);
// SECURITY: reject anything that is not a plain `category/name` slug.
if (!/^[A-Za-z0-9._/-]+$/.test(agentName)) {
task.output.push(`❌ Invalid agent name: ${agentName}`);
return Promise.resolve(false);
}
return new Promise((resolve, reject) => {
const child = spawn('npx', ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
// SECURITY: shell:false (default) — agentName stays a single argv entry.
const child = spawn(NPX_CMD, ['claude-code-templates@latest', '--agent', agentName, '--yes'], {
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
stdio: ['pipe', 'pipe', 'pipe']
});
child.stdout.on('data', (data) => {
@@ -460,15 +496,18 @@ async function executeLocalTask(task) {
finalPrompt = `As a ${task.agent.replace('-', ' ')}, ${task.prompt}`;
}
// Execute Claude Code locally with just the prompt
const child = spawn('claude', [finalPrompt], {
// Execute Claude Code locally with just the prompt.
// SECURITY: never run through a shell. With shell:true, Node joins argv into
// a single `sh -c` string and the attacker-controlled prompt is parsed by the
// shell (command injection). shell:false keeps finalPrompt as a single argv[1].
// Windows: resolve claude.cmd explicitly (CLAUDE_CMD) instead of re-enabling shell:true.
const child = spawn(CLAUDE_CMD, [finalPrompt], {
cwd: process.cwd(),
stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin to prevent hanging
env: {
...process.env,
PATH: process.env.PATH
},
shell: true, // Use shell to find claude command
timeout: 300000 // 5 minute timeout
});
@@ -558,8 +597,9 @@ async function executeLocalTask(task) {
}
}
// Start server
app.listen(PORT, () => {
// Start server — bind to loopback only. This is a local developer tool that
// executes commands; binding to 0.0.0.0 exposes RCE to the whole LAN.
app.listen(PORT, '127.0.0.1', () => {
console.log(chalk.blue('\\n🎨 Claude Code Studio Server'));
console.log(chalk.cyan('═══════════════════════════════════════'));
console.log(chalk.green(`🚀 Server running on http://localhost:${PORT}`));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-code-templates",
"version": "1.29.3",
"version": "1.29.4",
"description": "Component templates and tracking system for Claude Code",
"main": "cli-tool/src/index.js",
"bin": {