Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions src/cli/CodacyCli.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,8 +39,39 @@
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);

Check failure on line 57 in src/cli/CodacyCli.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cli/CodacyCli.ts#L57

Detected possible user input going into a `path.join` or `path.resolve` function.
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(
Expand All @@ -48,11 +80,20 @@
): 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(
Expand Down
29 changes: 22 additions & 7 deletions src/cli/WinWSLCodacyCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
): Promise<{ stdout: string; stderr: string }> {
return await super.execAsync(`wsl ${command}`, args);
return await super.execAsync(`wsl bash -c "${command}"`, args);
}

protected getCliCommand(): string {
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/cliAnalyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down