diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index bf93e83a4..8f72776d6 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -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; diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 4c2f2cebe..65ce7be88 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -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 { diff --git a/get-shit-done/bin/lib/workstream.cjs b/get-shit-done/bin/lib/workstream.cjs index 7a21c56bb..2aeaf63ec 100644 --- a/get-shit-done/bin/lib/workstream.cjs +++ b/get-shit-done/bin/lib/workstream.cjs @@ -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);