Skip to content

Commit d7db01c

Browse files
fix(security): add path traversal guard for CLI attachment paths (#183)
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: bgagent <345885+scottschreckengaust@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 08b91bf commit d7db01c

1 file changed

Lines changed: 16 additions & 3 deletions

File tree

cli/src/commands/submit.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,12 @@ export function makeSubmitCommand(): Command {
197197
throw new CliError(`No local file found for upload instruction: ${instruction.filename}`);
198198
}
199199
const filePath = attachmentArgs.find(arg =>
200-
!arg.startsWith('http') && path.basename(path.resolve(arg)) === instruction.filename,
200+
!arg.startsWith('http') && path.basename(safeResolvePath(arg)) === instruction.filename,
201201
);
202202
if (!filePath) {
203203
throw new CliError(`Cannot locate local file for presigned upload: ${instruction.filename}`);
204204
}
205-
await uploadViaPresignedPost(path.resolve(filePath), instruction);
205+
await uploadViaPresignedPost(safeResolvePath(filePath), instruction);
206206
process.stderr.write(` Uploaded: ${instruction.filename}\n`);
207207
}
208208

@@ -227,6 +227,19 @@ export function makeSubmitCommand(): Command {
227227
// Attachment resolution helpers
228228
// ---------------------------------------------------------------------------
229229

230+
/** Resolve a user-supplied path and validate it stays under CWD (CWE-22 mitigation). */
231+
function safeResolvePath(userPath: string): string {
232+
// nosemgrep: path-join-resolve-traversal -- this IS the path-traversal guard
233+
const resolved = path.resolve(userPath);
234+
const cwd = process.cwd();
235+
if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
236+
throw new CliError(
237+
`Attachment path must be within the working directory: ${userPath}`,
238+
);
239+
}
240+
return resolved;
241+
}
242+
230243
const MAX_INLINE_SIZE_BYTES = 500 * 1024; // 500 KB
231244

232245
/** MIME type lookup by file extension. */
@@ -263,7 +276,7 @@ function resolveAttachmentArg(arg: string): Attachment {
263276
}
264277

265278
// Local file
266-
const resolvedPath = path.resolve(arg);
279+
const resolvedPath = safeResolvePath(arg);
267280
if (!fs.existsSync(resolvedPath)) {
268281
throw new CliError(`Attachment file not found: ${arg}`);
269282
}

0 commit comments

Comments
 (0)