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.
- package/README.md +165 -5
- package/package.json +1 -1
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/access_control_system/__init__.py +38 -0
- package/src/zexus/access_control_system/access_control.py +237 -0
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/debug_sanitizer.py +250 -0
- package/src/zexus/error_reporter.py +22 -2
- package/src/zexus/evaluator/core.py +17 -21
- package/src/zexus/evaluator/expressions.py +116 -57
- package/src/zexus/evaluator/functions.py +613 -170
- package/src/zexus/evaluator/resource_limiter.py +291 -0
- package/src/zexus/evaluator/statements.py +47 -12
- package/src/zexus/evaluator/utils.py +12 -6
- package/src/zexus/lsp/server.py +1 -1
- package/src/zexus/object.py +21 -2
- package/src/zexus/parser/parser.py +56 -4
- package/src/zexus/parser/strategy_context.py +83 -7
- package/src/zexus/parser/strategy_structural.py +12 -4
- package/src/zexus/persistence.py +105 -6
- package/src/zexus/security.py +43 -25
- package/src/zexus/security_enforcement.py +237 -0
- package/src/zexus/stdlib/fs.py +120 -22
- package/src/zexus/zexus_ast.py +3 -2
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +499 -13
- package/src/zexus.egg-info/SOURCES.txt +258 -152
|
@@ -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
|
-
|
|
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} {
|
|
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
|
-
|
|
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
|
-
#
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
320
|
+
# SECURITY FIX #8: Type Safety - No Implicit Coercion
|
|
321
|
+
# Addition operator: String+String OR Number+Number only
|
|
281
322
|
elif operator == "+":
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
#
|
|
301
|
-
if isinstance(left, (Integer, Float)):
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
#
|
|
319
|
-
|
|
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
|
-
#
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|