From 1c39cf50dec8d5c4b0b4e7ec61d7978d07e4581d Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Tue, 26 May 2026 20:20:42 +0000 Subject: [PATCH] fix(security): add path traversal guard for CLI attachment paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces safeResolvePath() that validates resolved paths stay within the working directory (CWE-22 mitigation). All path.resolve(userInput) call sites in the attachment upload flow now go through this guard. The single nosemgrep suppression is on the guard function itself — it must call path.resolve before it can validate the result. Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/src/commands/submit.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/submit.ts b/cli/src/commands/submit.ts index 00d3ffa..f01a3f0 100644 --- a/cli/src/commands/submit.ts +++ b/cli/src/commands/submit.ts @@ -197,12 +197,12 @@ export function makeSubmitCommand(): Command { throw new CliError(`No local file found for upload instruction: ${instruction.filename}`); } const filePath = attachmentArgs.find(arg => - !arg.startsWith('http') && path.basename(path.resolve(arg)) === instruction.filename, + !arg.startsWith('http') && path.basename(safeResolvePath(arg)) === instruction.filename, ); if (!filePath) { throw new CliError(`Cannot locate local file for presigned upload: ${instruction.filename}`); } - await uploadViaPresignedPost(path.resolve(filePath), instruction); + await uploadViaPresignedPost(safeResolvePath(filePath), instruction); process.stderr.write(` Uploaded: ${instruction.filename}\n`); } @@ -227,6 +227,19 @@ export function makeSubmitCommand(): Command { // Attachment resolution helpers // --------------------------------------------------------------------------- +/** Resolve a user-supplied path and validate it stays under CWD (CWE-22 mitigation). */ +function safeResolvePath(userPath: string): string { + // nosemgrep: path-join-resolve-traversal -- this IS the path-traversal guard + const resolved = path.resolve(userPath); + const cwd = process.cwd(); + if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) { + throw new CliError( + `Attachment path must be within the working directory: ${userPath}`, + ); + } + return resolved; +} + const MAX_INLINE_SIZE_BYTES = 500 * 1024; // 500 KB /** MIME type lookup by file extension. */ @@ -263,7 +276,7 @@ function resolveAttachmentArg(arg: string): Attachment { } // Local file - const resolvedPath = path.resolve(arg); + const resolvedPath = safeResolvePath(arg); if (!fs.existsSync(resolvedPath)) { throw new CliError(`Attachment file not found: ${arg}`); }