mirror of
https://github.com/davila7/claude-code-templates.git
synced 2026-10-02 03:14:35 +08:00
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:
co-authored by
spartan8806
Claude Opus
Dani
parent
acdb589341
commit
bc4618b072
@@ -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,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": {
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user