@@ -133,30 +133,40 @@ enum CommandValidator {
133133 }
134134 }
135135
136- // 3. Block dangerous functions
137- let dangerousFunctions = [
138- " exec( " ,
139- " eval( " ,
140- " compile( " ,
141- " pickle.load( " ,
142- " pickle .loads( " ,
143- " torch.load( " ,
144- " open( " , // File access
145- " input( " , // User input
146- " raw_input( "
136+ // 3. Block dangerous functions using regex (consistent with import validation above)
137+ // Each tuple: (regex pattern, human-readable description)
138+ let dangerousFunctionPatterns : [ ( pattern : String , description : String ) ] = [
139+ ( #"\bexec\s*\("# , " exec() " ) ,
140+ ( #"\beval\s*\("# , " eval() " ) ,
141+ ( #"\bcompile\s*\("# , " compile() " ) ,
142+ ( #"\bpickle\ .loads?\s*\("# , " pickle.load(s) " ) ,
143+ ( #"\btorch\.load\s*\("# , " torch.load() " ) ,
144+ ( #"\bopen\s*\("# , " open() " ) ,
145+ ( #"\binput\s*\("# , " input() " ) ,
146+ ( #"\braw_input\s*\("# , " raw_input() " )
147147 ]
148148
149- for dangerous in dangerousFunctions {
150- if code. contains ( dangerous) {
151- logSecurityEvent ( " BLOCKED dangerous Python function: \( dangerous) " , level: . critical)
152- throw SecurityError . dangerousFunction ( dangerous)
149+ for (regexPattern, description) in dangerousFunctionPatterns {
150+ if let regex = try ? NSRegularExpression ( pattern: regexPattern, options: [ ] ) ,
151+ regex. firstMatch ( in: uncommentedCode, range: NSRange ( uncommentedCode. startIndex... , in: uncommentedCode) ) != nil {
152+ logSecurityEvent ( " BLOCKED dangerous Python function: \( description) " , level: . critical)
153+ throw SecurityError . dangerousFunction ( description)
153154 }
154155 }
155156
156- // 4. Block system manipulation
157- if code. contains ( " os. " ) || code. contains ( " sys. " ) || code. contains ( " subprocess. " ) {
158- logSecurityEvent ( " BLOCKED system manipulation in Python code " , level: . critical)
159- throw SecurityError . systemManipulation
157+ // 4. Block system manipulation using regex for consistency
158+ let systemPatterns : [ ( pattern: String , description: String ) ] = [
159+ ( #"\bos\."# , " os module access " ) ,
160+ ( #"\bsys\."# , " sys module access " ) ,
161+ ( #"\bsubprocess\."# , " subprocess module access " )
162+ ]
163+
164+ for (regexPattern, description) in systemPatterns {
165+ if let regex = try ? NSRegularExpression ( pattern: regexPattern, options: [ ] ) ,
166+ regex. firstMatch ( in: uncommentedCode, range: NSRange ( uncommentedCode. startIndex... , in: uncommentedCode) ) != nil {
167+ logSecurityEvent ( " BLOCKED system manipulation: \( description) " , level: . critical)
168+ throw SecurityError . systemManipulation
169+ }
160170 }
161171
162172 // 5. Log for audit
@@ -184,6 +194,12 @@ enum CommandValidator {
184194 throw SecurityError . fileNotFound ( expandedPath)
185195 }
186196
197+ // 2b. Verify file is readable (handles permission errors explicitly)
198+ guard FileManager . default. isReadableFile ( atPath: expandedPath) else {
199+ logSecurityEvent ( " Permission denied reading script: \( expandedPath) " , level: . error)
200+ throw SecurityError . invalidPath ( " Permission denied: cannot read \( expandedPath) " )
201+ }
202+
187203 // 3. Verify it's a Python file
188204 guard expandedPath. hasSuffix ( " .py " ) else {
189205 throw SecurityError . invalidFileType ( " Only .py files allowed " )
@@ -260,24 +276,41 @@ enum CommandValidator {
260276
261277 // MARK: - Logging
262278
279+ /// Serial queue for async file I/O to avoid blocking the calling thread
280+ private static let logQueue = DispatchQueue ( label: " com.mlxcode.commandvalidator.log " )
281+
263282 private static func logSecurityEvent( _ message: String , level: SecurityLogLevel ) {
264283 let timestamp = ISO8601DateFormatter ( ) . string ( from: Date ( ) )
265284 let logMessage = " [ \( timestamp) ] [CommandValidator] [ \( level. rawValue) ] \( message) "
266285
267- print ( logMessage)
286+ // Delegate to SecureLogger for structured logging (non-blocking fire-and-forget)
287+ Task {
288+ switch level {
289+ case . critical:
290+ await SecureLogger . shared. critical ( logMessage, category: " CommandValidator " )
291+ case . error:
292+ await SecureLogger . shared. error ( logMessage, category: " CommandValidator " )
293+ case . warning:
294+ await SecureLogger . shared. warning ( logMessage, category: " CommandValidator " )
295+ case . info:
296+ await SecureLogger . shared. info ( logMessage, category: " CommandValidator " )
297+ }
298+ }
268299
269- // Also log to security log file
300+ // Also write to security log file asynchronously (avoids blocking the caller)
270301 let logPath = NSHomeDirectory ( ) + " /Library/Logs/MLXCode/security.log "
271302 if let data = ( logMessage + " \n " ) . data ( using: . utf8) {
272- if let fileHandle = FileHandle ( forWritingAtPath: logPath) {
273- fileHandle. seekToEndOfFile ( )
274- fileHandle. write ( data)
275- try ? fileHandle. close ( )
276- } else {
277- // Create file if doesn't exist
278- let logDir = ( logPath as NSString ) . deletingLastPathComponent
279- try ? FileManager . default. createDirectory ( atPath: logDir, withIntermediateDirectories: true )
280- try ? data. write ( to: URL ( fileURLWithPath: logPath) )
303+ logQueue. async {
304+ if let fileHandle = FileHandle ( forWritingAtPath: logPath) {
305+ fileHandle. seekToEndOfFile ( )
306+ fileHandle. write ( data)
307+ try ? fileHandle. close ( )
308+ } else {
309+ // Create file if doesn't exist
310+ let logDir = ( logPath as NSString ) . deletingLastPathComponent
311+ try ? FileManager . default. createDirectory ( atPath: logDir, withIntermediateDirectories: true )
312+ try ? data. write ( to: URL ( fileURLWithPath: logPath) )
313+ }
281314 }
282315 }
283316 }
0 commit comments