zexus 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ """
2
+ Zexus Debug Information Sanitizer
3
+
4
+ Security Fix #10: Debug Info Sanitization
5
+ Prevents sensitive information from leaking through debug output and error messages.
6
+ """
7
+
8
+ import re
9
+ import os
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ class DebugSanitizer:
14
+ """
15
+ Sanitizes debug information to prevent sensitive data leakage.
16
+
17
+ Removes or masks:
18
+ - Passwords and API keys
19
+ - Database connection strings
20
+ - File paths (optional, based on mode)
21
+ - Environment variables
22
+ - Stack traces (in production mode)
23
+ """
24
+
25
+ # Patterns for sensitive data
26
+ SENSITIVE_PATTERNS = [
27
+ # Passwords
28
+ (re.compile(r'password\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'password=***'),
29
+ (re.compile(r'passwd\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'passwd=***'),
30
+ (re.compile(r'pwd\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'pwd=***'),
31
+
32
+ # API Keys and Tokens
33
+ (re.compile(r'api[_-]?key\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'api_key=***'),
34
+ (re.compile(r'secret[_-]?key\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'secret_key=***'),
35
+ (re.compile(r'auth[_-]?token\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'auth_token=***'),
36
+ (re.compile(r'access[_-]?token\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), 'access_token=***'),
37
+ (re.compile(r'bearer\s+([a-zA-Z0-9_\-\.]+)', re.IGNORECASE), 'bearer ***'),
38
+
39
+ # Database credentials
40
+ (re.compile(r'mysql://([^:]+):([^@]+)@', re.IGNORECASE), 'mysql://***:***@'),
41
+ (re.compile(r'postgres://([^:]+):([^@]+)@', re.IGNORECASE), 'postgres://***:***@'),
42
+ (re.compile(r'mongodb://([^:]+):([^@]+)@', re.IGNORECASE), 'mongodb://***:***@'),
43
+
44
+ # Generic key=value patterns with sensitive keywords
45
+ (re.compile(r'(private[_-]?key)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), r'\1=***'),
46
+ (re.compile(r'(encryption[_-]?key)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), r'\1=***'),
47
+ (re.compile(r'(client[_-]?secret)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', re.IGNORECASE), r'\1=***'),
48
+ ]
49
+
50
+ # Environment variables that should be masked
51
+ SENSITIVE_ENV_VARS = {
52
+ 'PASSWORD', 'SECRET', 'TOKEN', 'KEY', 'API_KEY', 'DB_PASSWORD',
53
+ 'DATABASE_PASSWORD', 'MYSQL_PASSWORD', 'POSTGRES_PASSWORD',
54
+ 'MONGODB_PASSWORD', 'REDIS_PASSWORD', 'AWS_SECRET_ACCESS_KEY',
55
+ 'PRIVATE_KEY', 'ENCRYPTION_KEY', 'JWT_SECRET'
56
+ }
57
+
58
+ def __init__(self, production_mode: bool = None):
59
+ """
60
+ Initialize sanitizer
61
+
62
+ Args:
63
+ production_mode: If True, applies stricter sanitization.
64
+ If None, auto-detects from environment.
65
+ """
66
+ if production_mode is None:
67
+ # Auto-detect from environment
68
+ env_mode = os.environ.get('ZEXUS_ENV', 'development').lower()
69
+ production_mode = env_mode in ('production', 'prod')
70
+
71
+ self.production_mode = production_mode
72
+
73
+ def sanitize_message(self, message: str) -> str:
74
+ """
75
+ Sanitize a message by removing sensitive information
76
+
77
+ Args:
78
+ message: The message to sanitize
79
+
80
+ Returns:
81
+ Sanitized message
82
+ """
83
+ if not isinstance(message, str):
84
+ message = str(message)
85
+
86
+ result = message
87
+
88
+ # Apply all sensitive patterns
89
+ for pattern, replacement in self.SENSITIVE_PATTERNS:
90
+ result = pattern.sub(replacement, result)
91
+
92
+ # Sanitize file paths in production mode
93
+ if self.production_mode:
94
+ result = self._sanitize_file_paths(result)
95
+
96
+ return result
97
+
98
+ def sanitize_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
99
+ """
100
+ Sanitize a dictionary by masking sensitive values
101
+
102
+ Args:
103
+ data: Dictionary to sanitize
104
+
105
+ Returns:
106
+ Sanitized dictionary
107
+ """
108
+ if not isinstance(data, dict):
109
+ return data
110
+
111
+ result = {}
112
+ for key, value in data.items():
113
+ # Check if key indicates sensitive data
114
+ key_lower = key.lower()
115
+ is_sensitive = any(
116
+ sensitive in key_lower
117
+ for sensitive in ['password', 'secret', 'token', 'key', 'api']
118
+ )
119
+
120
+ if is_sensitive:
121
+ result[key] = '***'
122
+ elif isinstance(value, dict):
123
+ result[key] = self.sanitize_dict(value)
124
+ elif isinstance(value, list):
125
+ result[key] = self.sanitize_list(value)
126
+ elif isinstance(value, str):
127
+ result[key] = self.sanitize_message(value)
128
+ else:
129
+ result[key] = value
130
+
131
+ return result
132
+
133
+ def sanitize_list(self, data: List[Any]) -> List[Any]:
134
+ """Sanitize a list by sanitizing each element"""
135
+ if not isinstance(data, list):
136
+ return data
137
+
138
+ result = []
139
+ for item in data:
140
+ if isinstance(item, dict):
141
+ result.append(self.sanitize_dict(item))
142
+ elif isinstance(item, list):
143
+ result.append(self.sanitize_list(item))
144
+ elif isinstance(item, str):
145
+ result.append(self.sanitize_message(item))
146
+ else:
147
+ result.append(item)
148
+
149
+ return result
150
+
151
+ def sanitize_stack_trace(self, stack_trace: str) -> str:
152
+ """
153
+ Sanitize stack trace information
154
+
155
+ In production mode, removes internal file paths and limits detail.
156
+ """
157
+ if not self.production_mode:
158
+ # In development, show full stack trace but sanitize sensitive data
159
+ return self.sanitize_message(stack_trace)
160
+
161
+ # In production, provide minimal stack trace
162
+ lines = stack_trace.split('\n')
163
+ sanitized_lines = []
164
+
165
+ for line in lines:
166
+ # Keep error messages but sanitize them
167
+ if 'Error:' in line or 'Exception:' in line:
168
+ sanitized_lines.append(self.sanitize_message(line))
169
+ # Remove file system paths
170
+ elif not line.strip().startswith('File '):
171
+ sanitized_lines.append(line)
172
+
173
+ return '\n'.join(sanitized_lines)
174
+
175
+ def sanitize_environment(self, env_vars: Dict[str, str]) -> Dict[str, str]:
176
+ """
177
+ Sanitize environment variables
178
+
179
+ Masks sensitive environment variables.
180
+ """
181
+ result = {}
182
+ for key, value in env_vars.items():
183
+ # Check if it's a sensitive environment variable
184
+ key_upper = key.upper()
185
+ is_sensitive = any(
186
+ sensitive in key_upper
187
+ for sensitive in self.SENSITIVE_ENV_VARS
188
+ )
189
+
190
+ if is_sensitive:
191
+ result[key] = '***'
192
+ else:
193
+ result[key] = value
194
+
195
+ return result
196
+
197
+ def _sanitize_file_paths(self, text: str) -> str:
198
+ """
199
+ Remove or generalize file paths in production mode
200
+
201
+ Converts absolute paths to relative or generic paths.
202
+ """
203
+ # Replace home directory paths
204
+ home = os.path.expanduser('~')
205
+ text = text.replace(home, '~')
206
+
207
+ # Replace absolute paths with relative
208
+ import re
209
+ text = re.sub(r'/[a-zA-Z0-9_\-/]+/([a-zA-Z0-9_\-\.]+\.zx)', r'./\1', text)
210
+
211
+ return text
212
+
213
+ def should_show_debug_info(self) -> bool:
214
+ """
215
+ Check if debug information should be shown
216
+
217
+ Returns False in production mode.
218
+ """
219
+ return not self.production_mode
220
+
221
+
222
+ # Global sanitizer instance
223
+ _sanitizer = DebugSanitizer()
224
+
225
+
226
+ def get_sanitizer() -> DebugSanitizer:
227
+ """Get the global debug sanitizer instance"""
228
+ return _sanitizer
229
+
230
+
231
+ def set_production_mode(enabled: bool):
232
+ """Set production mode globally"""
233
+ global _sanitizer
234
+ _sanitizer = DebugSanitizer(production_mode=enabled)
235
+
236
+
237
+ def sanitize_debug_output(message: str) -> str:
238
+ """Quick function to sanitize debug output"""
239
+ return _sanitizer.sanitize_message(message)
240
+
241
+
242
+ def sanitize_error_data(data: Any) -> Any:
243
+ """Quick function to sanitize error data"""
244
+ if isinstance(data, dict):
245
+ return _sanitizer.sanitize_dict(data)
246
+ elif isinstance(data, list):
247
+ return _sanitizer.sanitize_list(data)
248
+ elif isinstance(data, str):
249
+ return _sanitizer.sanitize_message(data)
250
+ return data
@@ -3,6 +3,8 @@ Zexus Error Reporting System
3
3
 
4
4
  Provides clear, beginner-friendly error messages that distinguish between
5
5
  user code errors and interpreter bugs.
6
+
7
+ Security Fix #10: Debug info sanitization to prevent sensitive data leakage.
6
8
  """
7
9
 
8
10
  import sys
@@ -10,6 +12,14 @@ from typing import Optional, List, Dict, Any
10
12
  from enum import Enum
11
13
 
12
14
 
15
+ # Import debug sanitizer for Security Fix #10
16
+ try:
17
+ from .debug_sanitizer import get_sanitizer
18
+ _SANITIZER_AVAILABLE = True
19
+ except ImportError:
20
+ _SANITIZER_AVAILABLE = False
21
+
22
+
13
23
  class ErrorCategory(Enum):
14
24
  """Categories of errors in Zexus"""
15
25
  USER_CODE = "user_code" # Error in user's Zexus code
@@ -106,12 +116,22 @@ class ZexusError(Exception):
106
116
  parts.append("")
107
117
 
108
118
  # Error message
109
- parts.append(f" {self.message}")
119
+ message = self.message
120
+ # Security Fix #10: Sanitize error messages
121
+ if _SANITIZER_AVAILABLE:
122
+ sanitizer = get_sanitizer()
123
+ message = sanitizer.sanitize_message(message)
124
+
125
+ parts.append(f" {message}")
110
126
 
111
127
  # Suggestion
112
128
  if self.suggestion:
129
+ suggestion = self.suggestion
130
+ # Sanitize suggestions too
131
+ if _SANITIZER_AVAILABLE:
132
+ suggestion = get_sanitizer().sanitize_message(suggestion)
113
133
  parts.append("")
114
- parts.append(f" {YELLOW}💡 Suggestion:{RESET} {self.suggestion}")
134
+ parts.append(f" {YELLOW}💡 Suggestion:{RESET} {suggestion}")
115
135
 
116
136
  # Internal error note
117
137
  if self.category == ErrorCategory.INTERPRETER:
@@ -8,6 +8,7 @@ from .expressions import ExpressionEvaluatorMixin
8
8
  from .statements import StatementEvaluatorMixin
9
9
  from .functions import FunctionEvaluatorMixin
10
10
  from .integration import EvaluationContext, get_integration
11
+ from .resource_limiter import ResourceLimiter, ResourceError, TimeoutError
11
12
 
12
13
  # Import VM and bytecode compiler
13
14
  try:
@@ -22,7 +23,7 @@ except ImportError as e:
22
23
  print(f"⚠️ VM not available in evaluator: {e}")
23
24
 
24
25
  class Evaluator(ExpressionEvaluatorMixin, StatementEvaluatorMixin, FunctionEvaluatorMixin):
25
- def __init__(self, trusted: bool = False, use_vm: bool = True):
26
+ def __init__(self, trusted: bool = False, use_vm: bool = True, resource_limiter=None):
26
27
  # Initialize mixins (FunctionEvaluatorMixin sets up builtins)
27
28
  FunctionEvaluatorMixin.__init__(self)
28
29
 
@@ -35,6 +36,9 @@ class Evaluator(ExpressionEvaluatorMixin, StatementEvaluatorMixin, FunctionEvalu
35
36
  else:
36
37
  self.integration_context.setup_for_untrusted_code()
37
38
 
39
+ # Resource limiting (Security Fix #7)
40
+ self.resource_limiter = resource_limiter or ResourceLimiter()
41
+
38
42
  # VM integration
39
43
  self.use_vm = use_vm and VM_AVAILABLE
40
44
  self.vm_instance = None
@@ -409,7 +413,8 @@ class Evaluator(ExpressionEvaluatorMixin, StatementEvaluatorMixin, FunctionEvalu
409
413
  value = value.replace('\\\\', '\\')
410
414
  value = value.replace('\\"', '"')
411
415
  value = value.replace("\\'", "'")
412
- return String(value)
416
+ # String literals are trusted (not from external input)
417
+ return String(value, is_trusted=True)
413
418
 
414
419
  elif isinstance(node, zexus_ast.Boolean):
415
420
  debug_log(" Boolean node", f"value: {node.value}")
@@ -512,30 +517,21 @@ class Evaluator(ExpressionEvaluatorMixin, StatementEvaluatorMixin, FunctionEvalu
512
517
  if is_error(obj):
513
518
  return obj
514
519
 
515
- # Safely extract property name - property can be Identifier, IntegerLiteral, or other expression
516
- # IMPORTANT: For Identifier nodes in index expressions (arr[i]), we need to evaluate them first!
517
- if isinstance(node.property, zexus_ast.Identifier):
518
- # This could be either a property name (obj.prop) or an index variable (arr[i])
519
- # We need to check if it's being used as an index (numeric) or property (string)
520
- # First try to evaluate it as an identifier (variable lookup)
521
- prop_result = self.eval_identifier(node.property, env)
522
- if not is_error(prop_result) and not isinstance(prop_result, type(NULL)):
523
- # Successfully found a variable, use its value as the property/index
524
- property_name = prop_result.value if hasattr(prop_result, 'value') else str(prop_result)
525
- else:
526
- # Not found as variable, treat as literal property name (for obj.prop syntax)
527
- property_name = node.property.value
528
- elif isinstance(node.property, zexus_ast.IntegerLiteral):
529
- # Direct integer index like arr[0]
530
- property_name = node.property.value
531
- elif isinstance(node.property, zexus_ast.PropertyAccessExpression):
532
- # Nested property access - evaluate it
520
+ # Determine property name based on whether it's computed (obj[expr]) or literal (obj.prop)
521
+ if hasattr(node, 'computed') and node.computed:
522
+ # Computed property (obj[expr]) - always evaluate the expression
533
523
  prop_result = self.eval_node(node.property, env, stack_trace)
534
524
  if is_error(prop_result):
535
525
  return prop_result
536
526
  property_name = prop_result.value if hasattr(prop_result, 'value') else str(prop_result)
527
+ elif isinstance(node.property, zexus_ast.Identifier):
528
+ # Literal property (obj.prop) - use the identifier name directly
529
+ property_name = node.property.value
530
+ elif isinstance(node.property, zexus_ast.IntegerLiteral):
531
+ # Direct integer index like arr[0] (for backwards compatibility)
532
+ property_name = node.property.value
537
533
  else:
538
- # Evaluate the property expression to get the key
534
+ # Fallback: evaluate the property expression
539
535
  prop_result = self.eval_node(node.property, env, stack_trace)
540
536
  if is_error(prop_result):
541
537
  return prop_result
@@ -108,19 +108,38 @@ class ExpressionEvaluatorMixin:
108
108
  left_val = left.value
109
109
  right_val = right.value
110
110
 
111
- if operator == "+":
112
- return Integer(left_val + right_val)
113
- elif operator == "-":
114
- return Integer(left_val - right_val)
115
- elif operator == "*":
116
- return Integer(left_val * right_val)
111
+ # SECURITY FIX #6: Integer overflow protection
112
+ # Python integers have arbitrary precision, but we enforce safe ranges
113
+ # to prevent resource exhaustion and match real-world integer behavior
114
+ MAX_SAFE_INT = 2**63 - 1 # 64-bit signed integer max
115
+ MIN_SAFE_INT = -(2**63) # 64-bit signed integer min
116
+
117
+ def check_overflow(result, operation):
118
+ """Check if integer operation resulted in overflow"""
119
+ if result > MAX_SAFE_INT or result < MIN_SAFE_INT:
120
+ return EvaluationError(
121
+ f"Integer overflow in {operation}",
122
+ suggestion=f"Result {result} exceeds safe integer range [{MIN_SAFE_INT}, {MAX_SAFE_INT}]. Use require() to validate inputs or break calculation into smaller parts."
123
+ )
124
+ return Integer(result)
125
+
126
+ if operator == "+":
127
+ result = left_val + right_val
128
+ return check_overflow(result, "addition")
129
+ elif operator == "-":
130
+ result = left_val - right_val
131
+ return check_overflow(result, "subtraction")
132
+ elif operator == "*":
133
+ result = left_val * right_val
134
+ return check_overflow(result, "multiplication")
117
135
  elif operator == "/":
118
136
  if right_val == 0:
119
137
  return EvaluationError(
120
138
  "Division by zero",
121
139
  suggestion="Check your divisor value. Consider adding a condition: if (divisor != 0) { ... }"
122
140
  )
123
- return Integer(left_val // right_val)
141
+ result = left_val // right_val
142
+ return check_overflow(result, "division")
124
143
  elif operator == "%":
125
144
  if right_val == 0:
126
145
  return EvaluationError(
@@ -173,8 +192,29 @@ class ExpressionEvaluatorMixin:
173
192
  return EvaluationError(f"Unknown float operator: {operator}")
174
193
 
175
194
  def eval_string_infix(self, operator, left, right):
176
- if operator == "+":
177
- return String(left.value + right.value)
195
+ if operator == "+":
196
+ # SECURITY ENFORCEMENT: Check sanitization before concatenation
197
+ from ..security_enforcement import check_string_concatenation
198
+ check_string_concatenation(left, right)
199
+
200
+ # Propagate sanitization status to result
201
+ result = String(left.value + right.value)
202
+
203
+ # Result is trusted only if both inputs are trusted
204
+ result.is_trusted = left.is_trusted and right.is_trusted
205
+
206
+ # Propagate sanitization context intelligently:
207
+ # - If both have same sanitization, inherit it
208
+ # - If one is trusted (literal) and other is sanitized, inherit the sanitization
209
+ # - Otherwise, no sanitization (both must be sanitized or one trusted + one sanitized)
210
+ if left.sanitized_for == right.sanitized_for and left.sanitized_for is not None:
211
+ result.sanitized_for = left.sanitized_for
212
+ elif left.is_trusted and right.sanitized_for is not None:
213
+ result.sanitized_for = right.sanitized_for
214
+ elif right.is_trusted and left.sanitized_for is not None:
215
+ result.sanitized_for = left.sanitized_for
216
+
217
+ return result
178
218
  elif operator == "==":
179
219
  return TRUE if left.value == right.value else FALSE
180
220
  elif operator == "!=":
@@ -277,46 +317,67 @@ class ExpressionEvaluatorMixin:
277
317
  else:
278
318
  return EvaluationError(f"Unsupported operation: {left.type()} {operator} DATETIME")
279
319
 
280
- # Mixed String Concatenation
320
+ # SECURITY FIX #8: Type Safety - No Implicit Coercion
321
+ # Addition operator: String+String OR Number+Number only
281
322
  elif operator == "+":
282
- if isinstance(left, String):
283
- right_str = right.inspect() if not isinstance(right, String) else right.value
284
- return String(left.value + str(right_str))
285
- elif isinstance(right, String):
286
- left_str = left.inspect() if not isinstance(left, String) else left.value
287
- return String(str(left_str) + right.value)
288
- # Mixed Numeric
323
+ # String concatenation requires both operands to be strings
324
+ if isinstance(left, String) or isinstance(right, String):
325
+ # If either is a string, both must be strings
326
+ if not (isinstance(left, String) and isinstance(right, String)):
327
+ left_type = "STRING" if isinstance(left, String) else type(left).__name__.upper()
328
+ right_type = "STRING" if isinstance(right, String) else type(right).__name__.upper()
329
+
330
+ return EvaluationError(
331
+ f"Type mismatch: cannot add {left_type} and {right_type}\n"
332
+ f"Use explicit conversion: string(value) to convert to string before concatenation\n"
333
+ f"Example: string({left.value if hasattr(left, 'value') else left}) + string({right.value if hasattr(right, 'value') else right})"
334
+ )
335
+
336
+ # Numeric addition: Integer + Integer OR Float + Float OR Integer + Float
289
337
  elif isinstance(left, (Integer, Float)) and isinstance(right, (Integer, Float)):
290
- l_val = float(left.value)
291
- r_val = float(right.value)
292
- return Float(l_val + r_val)
338
+ # Mixed Integer/Float operations return Float
339
+ if isinstance(left, Float) or isinstance(right, Float):
340
+ result = float(left.value) + float(right.value)
341
+ return Float(result)
342
+ # Both integers handled by eval_integer_infix
343
+ else:
344
+ return EvaluationError("Internal error: Integer + Integer should be handled by eval_integer_infix")
345
+
346
+ # Invalid type combination
347
+ else:
348
+ left_type = type(left).__name__.replace("Obj", "").upper()
349
+ right_type = type(right).__name__.replace("Obj", "").upper()
350
+ return EvaluationError(
351
+ f"Type error: cannot add {left_type} and {right_type}\n"
352
+ f"Addition requires matching types: STRING + STRING or NUMBER + NUMBER"
353
+ )
293
354
 
294
- # Mixed arithmetic operations (String coerced to number for *, -, /, %)
355
+ # SECURITY FIX #8: Strict Type Checking for Arithmetic
356
+ # All arithmetic operations require numeric types (Integer or Float)
295
357
  elif operator in ("*", "-", "/", "%"):
296
- # Try to coerce strings to numbers for arithmetic
297
- l_val = None
298
- r_val = None
358
+ # Get type names for error messages
359
+ left_type = type(left).__name__.replace("Obj", "").upper()
360
+ right_type = type(right).__name__.replace("Obj", "").upper()
299
361
 
300
- # Get left value
301
- if isinstance(left, (Integer, Float)):
302
- l_val = float(left.value)
303
- elif isinstance(left, String):
304
- try:
305
- l_val = float(left.value)
306
- except ValueError:
307
- pass
362
+ # Only allow arithmetic between numbers (Integer or Float)
363
+ if not isinstance(left, (Integer, Float)):
364
+ return EvaluationError(
365
+ f"Type error: {operator} requires numeric operands, got {left_type}\n"
366
+ f"Use explicit conversion: int(value) or float(value)"
367
+ )
308
368
 
309
- # Get right value
310
- if isinstance(right, (Integer, Float)):
311
- r_val = float(right.value)
312
- elif isinstance(right, String):
313
- try:
314
- r_val = float(right.value)
315
- except ValueError:
316
- pass
369
+ if not isinstance(right, (Integer, Float)):
370
+ return EvaluationError(
371
+ f"Type error: {operator} requires numeric operands, got {right_type}\n"
372
+ f"Use explicit conversion: int(value) or float(value)"
373
+ )
317
374
 
318
- # Perform operation if both values could be coerced
319
- if l_val is not None and r_val is not None:
375
+ # Both are numbers - perform operation
376
+ # Mixed Integer/Float operations return Float
377
+ if isinstance(left, Float) or isinstance(right, Float):
378
+ l_val = float(left.value)
379
+ r_val = float(right.value)
380
+
320
381
  try:
321
382
  if operator == "*":
322
383
  result = l_val * r_val
@@ -332,13 +393,16 @@ class ExpressionEvaluatorMixin:
332
393
  result = l_val % r_val
333
394
 
334
395
  # Return Integer if result is whole number, Float otherwise
335
- if result == int(result):
396
+ if result == int(result) and operator != "/": # Division always returns float
336
397
  return Integer(int(result))
337
398
  return Float(result)
338
399
  except Exception as e:
339
400
  return EvaluationError(f"Arithmetic error: {str(e)}")
401
+ else:
402
+ # Both integers - integer arithmetic (already handled by eval_integer_infix)
403
+ return EvaluationError(f"Internal error: Integer {operator} Integer should be handled by eval_integer_infix")
340
404
 
341
- # Comparison with mixed numeric types
405
+ # Comparison with mixed numeric types (Integer/Float comparison allowed)
342
406
  elif operator in ("<", ">", "<=", ">="):
343
407
  if isinstance(left, (Integer, Float)) and isinstance(right, (Integer, Float)):
344
408
  l_val = float(left.value)
@@ -348,19 +412,14 @@ class ExpressionEvaluatorMixin:
348
412
  elif operator == "<=": return TRUE if l_val <= r_val else FALSE
349
413
  elif operator == ">=": return TRUE if l_val >= r_val else FALSE
350
414
 
351
- # Mixed String/Number comparison (Coerce to float)
352
- elif (isinstance(left, (Integer, Float)) and isinstance(right, String)) or \
353
- (isinstance(left, String) and isinstance(right, (Integer, Float))):
354
- try:
355
- l_val = float(left.value)
356
- r_val = float(right.value)
357
- if operator == "<": return TRUE if l_val < r_val else FALSE
358
- elif operator == ">": return TRUE if l_val > r_val else FALSE
359
- elif operator == "<=": return TRUE if l_val <= r_val else FALSE
360
- elif operator == ">=": return TRUE if l_val >= r_val else FALSE
361
- except ValueError:
362
- # If conversion fails, return FALSE (NaN comparison behavior)
363
- return FALSE
415
+ # SECURITY FIX #8: No implicit coercion for comparisons
416
+ else:
417
+ left_type = type(left).__name__.replace("Obj", "").upper()
418
+ right_type = type(right).__name__.replace("Obj", "").upper()
419
+ return EvaluationError(
420
+ f"Type error: cannot compare {left_type} {operator} {right_type}\n"
421
+ f"Use explicit conversion if needed: int(value) or float(value)"
422
+ )
364
423
 
365
424
  return EvaluationError(f"Type mismatch: {left.type()} {operator} {right.type()}")
366
425