fix: prevent path traversal via workstream name sanitization

Validates workstream name at all entry points — CLI --ws flag,
GSD_WORKSTREAM env var, active-workstream file, and cmdWorkstreamSet —
blocking names that don't match [a-zA-Z0-9_-]+. Also fixes
getActiveWorkstream to use planningRoot() consistently and validates
names read from the active-workstream file before using them in path
joins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SalesTeamToolbox
2026-03-20 21:58:48 -07:00
parent c83b69bbb5
commit e3a427252d
3 changed files with 12 additions and 3 deletions

View File

@@ -205,6 +205,10 @@ async function main() {
} else {
ws = getActiveWorkstream(cwd);
}
// Validate workstream name to prevent path traversal attacks.
if (ws && !/^[a-zA-Z0-9_-]+$/.test(ws)) {
error('Invalid workstream name: must be alphanumeric, hyphens, and underscores only');
}
// Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths
if (ws) {
process.env.GSD_WORKSTREAM = ws;

View File

@@ -568,11 +568,11 @@ function planningPaths(cwd, ws) {
* Returns null if no active workstream or file doesn't exist.
*/
function getActiveWorkstream(cwd) {
const filePath = path.join(cwd, '.planning', 'active-workstream');
const filePath = path.join(planningRoot(cwd), 'active-workstream');
try {
const name = fs.readFileSync(filePath, 'utf-8').trim();
if (!name) return null;
const wsDir = path.join(cwd, '.planning', 'workstreams', name);
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) return null;
const wsDir = path.join(planningRoot(cwd), 'workstreams', name);
if (!fs.existsSync(wsDir)) return null;
return name;
} catch {

View File

@@ -352,6 +352,11 @@ function cmdWorkstreamSet(cwd, name, raw) {
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
output({ active: null, error: 'invalid_name', message: 'Workstream name must be alphanumeric, hyphens, and underscores only' }, raw);
return;
}
const wsDir = path.join(planningRoot(cwd), 'workstreams', name);
if (!fs.existsSync(wsDir)) {
output({ active: null, error: 'not_found', workstream: name }, raw);