@@ -3,7 +3,8 @@ export const CODACY_FOLDER_NAME = '.codacy'
33import { exec } from 'child_process'
44import { Config } from '../common'
55import Logger from '../common/logger'
6- import { ProcessedSarifResult } from '.'
6+ import { ProcessedSarifResult } from './utils'
7+ import * as path from 'path'
78
89// Set a larger buffer size (10MB)
910const MAX_BUFFER_SIZE = 1024 * 1024 * 10
@@ -47,8 +48,80 @@ export abstract class CodacyCli {
4748 vscode . commands . executeCommand ( 'setContext' , 'codacy:cliInstalled' , ! ! command )
4849 }
4950
51+ /**
52+ * Validates a file path for security concerns
53+ *
54+ * Security measures:
55+ * 1. Rejects null bytes (\0) - always indicates malicious intent
56+ * 2. Rejects control characters (ASCII 0-31 and 127)
57+ * 3. Prevents path traversal attacks by ensuring paths resolve within workspace
58+ *
59+ * Allows:
60+ * - Unicode characters (e.g., emoji, non-English characters)
61+ * - Common special characters in file names (spaces, parentheses, brackets, etc.)
62+ * - Shell metacharacters (will be escaped by preparePathForExec)
63+ *
64+ * @param filePath The path to validate
65+ * @returns true if the path is safe, false otherwise
66+ */
67+ protected isPathSafe ( filePath : string ) : boolean {
68+ // Reject null bytes (always a security risk)
69+ if ( filePath . includes ( '\0' ) ) {
70+ Logger . warn ( `Path contains null byte: ${ filePath } ` )
71+ return false
72+ }
73+
74+ // Reject all control characters (including newline, tab, carriage return)
75+ // as they are very unusual for file names
76+ // eslint-disable-next-line no-control-regex -- Intentionally checking for control chars to reject them for security
77+ const hasUnsafeControlChars = / [ \x00 - \x1F \x7F ] / . test ( filePath )
78+ if ( hasUnsafeControlChars ) {
79+ Logger . warn ( `Path contains unsafe control characters: ${ filePath } ` )
80+ return false
81+ }
82+
83+ // Resolve the path to check for path traversal attempts
84+ const resolvedPath = path . resolve ( this . rootPath , filePath )
85+ const normalizedRoot = path . normalize ( this . rootPath )
86+ // Check if the resolved path is within the workspace
87+ if ( ! resolvedPath . startsWith ( normalizedRoot ) ) {
88+ Logger . warn ( `Path traversal attempt detected: ${ filePath } resolves outside workspace` )
89+ return false
90+ }
91+
92+ return true
93+ }
94+
95+ /**
96+ * Prepares a file path for safe shell execution by escaping special characters
97+ *
98+ * Security approach:
99+ * 1. First validates path using isPathSafe() (path traversal, null bytes, control chars)
100+ * 2. Then escapes all shell metacharacters to prevent command injection
101+ *
102+ * Escaped characters include:
103+ * - Command separators: ; & |
104+ * - Command substitution: ` $ ( )
105+ * - I/O redirection: < >
106+ * - Glob patterns: * ? [ ] { }
107+ * - Quotes and spaces: ' " \s
108+ * - Special shell chars: ~ \
109+ *
110+ * This allows legitimate file names with unusual but safe characters while
111+ * preventing command injection attacks.
112+ *
113+ * @param path The file path to prepare for shell execution
114+ * @returns Escaped path safe for shell execution
115+ * @throws Error if path fails security validation
116+ */
50117 protected preparePathForExec ( path : string ) : string {
51- return path
118+ // Validate path security before escaping
119+ if ( ! this . isPathSafe ( path ) ) {
120+ throw new Error ( `Unsafe file path rejected: ${ path } ` )
121+ }
122+
123+ // Escape special characters for shell execution
124+ return path . replace ( / ( [ \s ' " \\ ; & | ` $ ( ) [ \] { } * ? ~ < > ] ) / g, '\\$1' )
52125 }
53126
54127 protected getIdentificationParameters ( ) : Record < string , string > {
@@ -65,15 +138,24 @@ export abstract class CodacyCli {
65138 }
66139
67140 protected execAsync ( command : string , args ?: Record < string , string > ) : Promise < { stdout : string ; stderr : string } > {
68- // stringyfy the args
141+ // Validate and escape arguments
69142 const argsString = Object . entries ( args || { } )
70- . map ( ( [ key , value ] ) => `--${ key } ${ value } ` )
143+ . map ( ( [ key , value ] ) => {
144+ // Validate argument key (should be alphanumeric and hyphens only)
145+ if ( ! / ^ [ a - z A - Z 0 - 9 - ] + $ / . test ( key ) ) {
146+ throw new Error ( `Invalid argument key: ${ key } ` )
147+ }
148+
149+ // Escape the value to prevent injection
150+ const escapedValue = value . replace ( / ( [ \s ' " \\ ; & | ` $ ( ) [ \] { } * ? ~ < > ] ) / g, '\\$1' )
151+ return `--${ key } ${ escapedValue } `
152+ } )
71153 . join ( ' ' )
72154
73- // Add the args to the command and remove any shell metacharacters
74- const cmd = `${ command } ${ argsString } ` . trim ( ) . replace ( / [ ; & | ` $ ] / g , '' )
155+ // Build the command - no need to strip characters since we've already escaped them properly
156+ const cmd = `${ command } ${ argsString } ` . trim ( )
75157
76- // Add the CODACY_CLI_VERSION to the command
158+ // Execute command
77159 return new Promise ( ( resolve , reject ) => {
78160 exec (
79161 cmd ,
0 commit comments