From dd790da57b7fdc7a1055885f5f4f2323b73a5893 Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Fri, 20 Feb 2026 16:18:42 +0200 Subject: [PATCH 1/2] escape characters --- src/cli/CodacyCli.ts | 49 ++++++++++++++++++++++++++++++++++---- src/cli/WinWSLCodacyCli.ts | 29 ++++++++++++++++------ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/cli/CodacyCli.ts b/src/cli/CodacyCli.ts index a623a77..db288dd 100644 --- a/src/cli/CodacyCli.ts +++ b/src/cli/CodacyCli.ts @@ -1,6 +1,7 @@ export const CODACY_FOLDER_NAME = '.codacy'; import { exec } from 'child_process'; import { Log } from 'sarif'; +import * as path from 'path'; // Set a larger buffer size (10MB) const MAX_BUFFER_SIZE = 1024 * 1024 * 10; @@ -38,8 +39,39 @@ export abstract class CodacyCli { this._cliCommand = command; } + protected isPathSafe(filePath: string): boolean { + // Reject null bytes (always a security risk) + if (filePath.includes('\0')) { + return false; + } + + // Reject all control characters (including newline, tab, carriage return) + // as they are very unusual for file names + // eslint-disable-next-line no-control-regex -- Intentionally checking for control chars to reject them for security + const hasUnsafeControlChars = /[\x00-\x1F\x7F]/.test(filePath); + if (hasUnsafeControlChars) { + return false; + } + + // Resolve the path to check for path traversal attempts + const resolvedPath = path.resolve(this.rootPath, filePath); + const normalizedRoot = path.normalize(this.rootPath); + // Check if the resolved path is within the workspace + if (!resolvedPath.startsWith(normalizedRoot)) { + return false; + } + + return true; + } + protected preparePathForExec(path: string): string { - return path; + // Validate path security before escaping + if (!this.isPathSafe(path)) { + throw new Error(`Unsafe file path rejected: ${path}`); + } + + // Escape special characters for shell execution + return path.replace(/([\s'"\\;&|`$()[\]{}*?~<>])/g, '\\$1'); } protected execAsync( @@ -48,11 +80,20 @@ export abstract class CodacyCli { ): Promise<{ stdout: string; stderr: string }> { // stringyfy the args const argsString = Object.entries(args || {}) - .map(([key, value]) => `--${key} ${value}`) + .map(([key, value]) => { + // Validate argument key (should be alphanumeric and hyphens only) + if (!/^[a-zA-Z0-9-]+$/.test(key)) { + throw new Error(`Invalid argument key: ${key}`); + } + + // Escape the value to prevent injection + const escapedValue = value.replace(/([\s'"\\;&|`$()[\]{}*?~<>])/g, '\\$1'); + return `--${key} ${escapedValue}`; + }) .join(' '); - // Add the args to the command and remove any shell metacharacters - const cmd = `${command} ${argsString}`.trim().replace(/[;&|`$]/g, ''); + // Build the command - no need to strip characters since we've already escaped them properly + const cmd = `${command} ${argsString}`.trim(); return new Promise((resolve, reject) => { exec( diff --git a/src/cli/WinWSLCodacyCli.ts b/src/cli/WinWSLCodacyCli.ts index 2852d83..51e9aeb 100644 --- a/src/cli/WinWSLCodacyCli.ts +++ b/src/cli/WinWSLCodacyCli.ts @@ -9,29 +9,44 @@ export class WinWSLCodacyCli extends MacCodacyCli { } private static toWSLPath(path: string): string { - // Convert Windows path to WSL path - // Example: C:\Users\user\project -> /mnt/c/Users/user/project - const wslPath = path.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/mnt/$1'); + // First, remove outer quotes if present + const cleanPath = path.replace(/^["']|["']$/g, ''); + // Convert backslashes to slashes and handle drive letter + const wslPath = cleanPath + .replace(/\\/g, '/') + .replace(/^([a-zA-Z]):/, (match, letter) => `/mnt/${letter.toLowerCase()}`); return wslPath; } private static fromWSLPath(path: string): string { // Convert WSL path to Windows path // Example: /mnt/c/Users/user/project -> C:\Users\user\project - const windowsPath = path.replace(/^\/mnt\/([a-zA-Z])/, '$1:').replace(/\//g, '\\'); + const windowsPath = path + .replace(/^'\/mnt\/([a-zA-Z])/, (match, letter) => `'${letter.toUpperCase()}:`) + .replace(/^\/mnt\/([a-zA-Z])/, (match, letter) => `${letter.toUpperCase()}:`) + .replace(/\//g, '\\'); return windowsPath; } protected preparePathForExec(path: string): string { - // Convert the path to WSL format - return WinWSLCodacyCli.toWSLPath(path); + // Convert WSL path to Windows format for validation + const winFilePath = path.startsWith('/mnt/') ? WinWSLCodacyCli.fromWSLPath(path) : path; + + // Validate path security before escaping + if (!this.isPathSafe(winFilePath)) { + throw new Error(`Unsafe file path rejected: ${winFilePath}`); + } + // Convert to WSL format and escape special characters + const wslPath = WinWSLCodacyCli.toWSLPath(winFilePath); + const escapedPath = wslPath.replace(/([\s'"\\;&|`$()[\]{}*?~<>])/g, '\\$1'); + return `'${escapedPath}'`; } protected async execAsync( command: string, args?: Record ): Promise<{ stdout: string; stderr: string }> { - return await super.execAsync(`wsl ${command}`, args); + return await super.execAsync(`wsl bash -c "${command}"`, args); } protected getCliCommand(): string { From 0a52fc208ca4bbe13d62cf53332cd040b0dafd7d Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Fri, 20 Feb 2026 18:06:27 +0200 Subject: [PATCH 2/2] add quotes to file path --- src/handlers/cliAnalyze.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/cliAnalyze.ts b/src/handlers/cliAnalyze.ts index 98b7dcc..e0eba5c 100644 --- a/src/handlers/cliAnalyze.ts +++ b/src/handlers/cliAnalyze.ts @@ -5,7 +5,7 @@ export const cliAnalyzeHandler = async (args: any) => { try { const results = await cli.analyze({ - file: args.file, + file: `'${args.file}'`, tool: args.tool, });