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,291 @@
|
|
|
1
|
+
# src/zexus/evaluator/resource_limiter.py
|
|
2
|
+
"""
|
|
3
|
+
Resource Limiter for Zexus Interpreter
|
|
4
|
+
|
|
5
|
+
Prevents resource exhaustion attacks by enforcing limits on:
|
|
6
|
+
- Loop iterations (prevents infinite loops)
|
|
7
|
+
- Execution time (prevents DoS via slow operations)
|
|
8
|
+
- Call stack depth (prevents stack overflow)
|
|
9
|
+
- Memory usage (prevents memory exhaustion)
|
|
10
|
+
|
|
11
|
+
Security Fix #7: Resource Limits
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
import signal
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResourceError(Exception):
|
|
20
|
+
"""Raised when a resource limit is exceeded"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TimeoutError(ResourceError):
|
|
25
|
+
"""Raised when execution timeout is exceeded"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ResourceLimiter:
|
|
30
|
+
"""
|
|
31
|
+
Enforces resource limits during program execution.
|
|
32
|
+
|
|
33
|
+
Limits:
|
|
34
|
+
- max_iterations: Maximum loop iterations across all loops (default: 1,000,000)
|
|
35
|
+
- timeout_seconds: Maximum execution time (default: 30 seconds)
|
|
36
|
+
- max_call_depth: Maximum call stack depth (default: 1000)
|
|
37
|
+
- max_memory_mb: Maximum memory usage (default: 500 MB, not enforced by default)
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
limiter = ResourceLimiter(max_iterations=100000, timeout_seconds=10)
|
|
41
|
+
limiter.start() # Start timeout timer
|
|
42
|
+
|
|
43
|
+
# In loops:
|
|
44
|
+
limiter.check_iterations()
|
|
45
|
+
|
|
46
|
+
# On function calls:
|
|
47
|
+
limiter.enter_call()
|
|
48
|
+
# ... function body ...
|
|
49
|
+
limiter.exit_call()
|
|
50
|
+
|
|
51
|
+
limiter.stop() # Stop timeout timer
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Default limits
|
|
55
|
+
DEFAULT_MAX_ITERATIONS = 1_000_000 # 1 million iterations
|
|
56
|
+
DEFAULT_TIMEOUT_SECONDS = 30 # 30 seconds
|
|
57
|
+
DEFAULT_MAX_CALL_DEPTH = 100 # 100 nested calls (Python interpreter uses many stack frames per Zexus call)
|
|
58
|
+
DEFAULT_MAX_MEMORY_MB = 500 # 500 MB (not enforced by default)
|
|
59
|
+
|
|
60
|
+
def __init__(self,
|
|
61
|
+
max_iterations=None,
|
|
62
|
+
timeout_seconds=None,
|
|
63
|
+
max_call_depth=None,
|
|
64
|
+
max_memory_mb=None,
|
|
65
|
+
enable_timeout=False,
|
|
66
|
+
enable_memory_check=False):
|
|
67
|
+
"""
|
|
68
|
+
Initialize resource limiter.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
max_iterations: Maximum total loop iterations (default: 1,000,000)
|
|
72
|
+
timeout_seconds: Maximum execution time (default: 30)
|
|
73
|
+
max_call_depth: Maximum call stack depth (default: 1000)
|
|
74
|
+
max_memory_mb: Maximum memory usage in MB (default: 500)
|
|
75
|
+
enable_timeout: Enable timeout enforcement (default: False, Linux only)
|
|
76
|
+
enable_memory_check: Enable memory checking (default: False, requires psutil)
|
|
77
|
+
"""
|
|
78
|
+
self.max_iterations = max_iterations or self.DEFAULT_MAX_ITERATIONS
|
|
79
|
+
self.timeout_seconds = timeout_seconds or self.DEFAULT_TIMEOUT_SECONDS
|
|
80
|
+
self.max_call_depth = max_call_depth or self.DEFAULT_MAX_CALL_DEPTH
|
|
81
|
+
self.max_memory_mb = max_memory_mb or self.DEFAULT_MAX_MEMORY_MB
|
|
82
|
+
|
|
83
|
+
# Feature flags
|
|
84
|
+
self.enable_timeout = enable_timeout
|
|
85
|
+
self.enable_memory_check = enable_memory_check
|
|
86
|
+
|
|
87
|
+
# Runtime counters
|
|
88
|
+
self.iteration_count = 0
|
|
89
|
+
self.call_depth = 0
|
|
90
|
+
self.start_time = None
|
|
91
|
+
self.timeout_handler = None
|
|
92
|
+
|
|
93
|
+
# Memory checking (optional, requires psutil)
|
|
94
|
+
self.psutil_available = False
|
|
95
|
+
if enable_memory_check:
|
|
96
|
+
try:
|
|
97
|
+
import psutil
|
|
98
|
+
self.psutil = psutil
|
|
99
|
+
self.psutil_available = True
|
|
100
|
+
self.process = psutil.Process()
|
|
101
|
+
except ImportError:
|
|
102
|
+
print("⚠️ Warning: psutil not available, memory checking disabled")
|
|
103
|
+
self.enable_memory_check = False
|
|
104
|
+
|
|
105
|
+
def start(self):
|
|
106
|
+
"""
|
|
107
|
+
Start resource monitoring (timeout timer, etc.)
|
|
108
|
+
Should be called at the beginning of script execution.
|
|
109
|
+
"""
|
|
110
|
+
self.start_time = time.time()
|
|
111
|
+
self.iteration_count = 0
|
|
112
|
+
self.call_depth = 0
|
|
113
|
+
|
|
114
|
+
# Set timeout handler (Linux/Unix only)
|
|
115
|
+
if self.enable_timeout and hasattr(signal, 'SIGALRM'):
|
|
116
|
+
self._set_timeout_alarm()
|
|
117
|
+
|
|
118
|
+
def stop(self):
|
|
119
|
+
"""
|
|
120
|
+
Stop resource monitoring and cleanup.
|
|
121
|
+
Should be called at the end of script execution.
|
|
122
|
+
"""
|
|
123
|
+
# Cancel timeout alarm
|
|
124
|
+
if self.enable_timeout and hasattr(signal, 'SIGALRM'):
|
|
125
|
+
signal.alarm(0) # Cancel alarm
|
|
126
|
+
|
|
127
|
+
def reset(self):
|
|
128
|
+
"""Reset iteration counter (useful for multiple script executions)"""
|
|
129
|
+
self.iteration_count = 0
|
|
130
|
+
self.call_depth = 0
|
|
131
|
+
self.start_time = None
|
|
132
|
+
|
|
133
|
+
def check_iterations(self):
|
|
134
|
+
"""
|
|
135
|
+
Check if iteration limit has been exceeded.
|
|
136
|
+
Should be called at the beginning of each loop iteration.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ResourceError: If iteration limit exceeded
|
|
140
|
+
"""
|
|
141
|
+
self.iteration_count += 1
|
|
142
|
+
|
|
143
|
+
if self.iteration_count > self.max_iterations:
|
|
144
|
+
raise ResourceError(
|
|
145
|
+
f"Iteration limit exceeded: {self.max_iterations:,} iterations\n"
|
|
146
|
+
f"This prevents infinite loops and resource exhaustion.\n\n"
|
|
147
|
+
f"Suggestion: Review your loop conditions or increase the limit with:\n"
|
|
148
|
+
f" zx-run --max-iterations 10000000 script.zx"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def check_timeout(self):
|
|
152
|
+
"""
|
|
153
|
+
Check if execution timeout has been exceeded.
|
|
154
|
+
Should be called periodically during long operations.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
TimeoutError: If timeout exceeded
|
|
158
|
+
"""
|
|
159
|
+
if self.start_time is None:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
elapsed = time.time() - self.start_time
|
|
163
|
+
if elapsed > self.timeout_seconds:
|
|
164
|
+
raise TimeoutError(
|
|
165
|
+
f"Execution timeout exceeded: {elapsed:.2f}s > {self.timeout_seconds}s\n"
|
|
166
|
+
f"This prevents denial-of-service via slow operations.\n\n"
|
|
167
|
+
f"Suggestion: Optimize your code or increase timeout with:\n"
|
|
168
|
+
f" zx-run --timeout 60 script.zx"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def check_memory(self):
|
|
172
|
+
"""
|
|
173
|
+
Check if memory limit has been exceeded.
|
|
174
|
+
Should be called periodically (e.g., every 1000 iterations).
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ResourceError: If memory limit exceeded
|
|
178
|
+
"""
|
|
179
|
+
if not self.enable_memory_check or not self.psutil_available:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
memory_mb = self.process.memory_info().rss / 1024 / 1024
|
|
184
|
+
|
|
185
|
+
if memory_mb > self.max_memory_mb:
|
|
186
|
+
raise ResourceError(
|
|
187
|
+
f"Memory limit exceeded: {memory_mb:.2f}MB > {self.max_memory_mb}MB\n"
|
|
188
|
+
f"This prevents memory exhaustion attacks.\n\n"
|
|
189
|
+
f"Suggestion: Reduce memory usage or increase limit with:\n"
|
|
190
|
+
f" zx-run --max-memory 1000 script.zx"
|
|
191
|
+
)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
# Don't crash on memory check failure
|
|
194
|
+
print(f"⚠️ Warning: Memory check failed: {e}")
|
|
195
|
+
|
|
196
|
+
def enter_call(self, function_name=None):
|
|
197
|
+
"""
|
|
198
|
+
Called when entering a function/action call.
|
|
199
|
+
Tracks call depth to prevent stack overflow.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
function_name: Optional name of function being called
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ResourceError: If call depth limit exceeded
|
|
206
|
+
"""
|
|
207
|
+
self.call_depth += 1
|
|
208
|
+
|
|
209
|
+
if self.call_depth > self.max_call_depth:
|
|
210
|
+
func_info = f" ({function_name})" if function_name else ""
|
|
211
|
+
raise ResourceError(
|
|
212
|
+
f"Call depth limit exceeded: {self.max_call_depth} nested calls{func_info}\n"
|
|
213
|
+
f"This prevents stack overflow from excessive recursion.\n\n"
|
|
214
|
+
f"Suggestion: Review your recursion or increase limit with:\n"
|
|
215
|
+
f" zx-run --max-call-depth 5000 script.zx"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def exit_call(self):
|
|
219
|
+
"""
|
|
220
|
+
Called when exiting a function/action call.
|
|
221
|
+
Decrements call depth counter.
|
|
222
|
+
"""
|
|
223
|
+
if self.call_depth > 0:
|
|
224
|
+
self.call_depth -= 1
|
|
225
|
+
|
|
226
|
+
def get_stats(self):
|
|
227
|
+
"""
|
|
228
|
+
Get current resource usage statistics.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
dict: Resource usage stats
|
|
232
|
+
"""
|
|
233
|
+
stats = {
|
|
234
|
+
'iterations': self.iteration_count,
|
|
235
|
+
'max_iterations': self.max_iterations,
|
|
236
|
+
'iteration_percent': (self.iteration_count / self.max_iterations) * 100,
|
|
237
|
+
'call_depth': self.call_depth,
|
|
238
|
+
'max_call_depth': self.max_call_depth,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if self.start_time:
|
|
242
|
+
elapsed = time.time() - self.start_time
|
|
243
|
+
stats['elapsed_seconds'] = elapsed
|
|
244
|
+
stats['timeout_seconds'] = self.timeout_seconds
|
|
245
|
+
stats['timeout_percent'] = (elapsed / self.timeout_seconds) * 100
|
|
246
|
+
|
|
247
|
+
if self.enable_memory_check and self.psutil_available:
|
|
248
|
+
try:
|
|
249
|
+
memory_mb = self.process.memory_info().rss / 1024 / 1024
|
|
250
|
+
stats['memory_mb'] = memory_mb
|
|
251
|
+
stats['max_memory_mb'] = self.max_memory_mb
|
|
252
|
+
stats['memory_percent'] = (memory_mb / self.max_memory_mb) * 100
|
|
253
|
+
except:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
return stats
|
|
257
|
+
|
|
258
|
+
def _set_timeout_alarm(self):
|
|
259
|
+
"""
|
|
260
|
+
Set SIGALRM timeout handler (Linux/Unix only).
|
|
261
|
+
This is automatically called by start() if enable_timeout is True.
|
|
262
|
+
"""
|
|
263
|
+
def timeout_handler(signum, frame):
|
|
264
|
+
raise TimeoutError(
|
|
265
|
+
f"Execution timeout: {self.timeout_seconds}s exceeded\n"
|
|
266
|
+
f"This prevents denial-of-service via slow operations.\n\n"
|
|
267
|
+
f"Suggestion: Optimize your code or increase timeout with:\n"
|
|
268
|
+
f" zx-run --timeout 60 script.zx"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
self.timeout_handler = timeout_handler
|
|
272
|
+
signal.signal(signal.SIGALRM, timeout_handler)
|
|
273
|
+
signal.alarm(self.timeout_seconds)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# Default global limiter (can be overridden)
|
|
277
|
+
_default_limiter = None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def get_default_limiter():
|
|
281
|
+
"""Get the default global resource limiter"""
|
|
282
|
+
global _default_limiter
|
|
283
|
+
if _default_limiter is None:
|
|
284
|
+
_default_limiter = ResourceLimiter()
|
|
285
|
+
return _default_limiter
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def set_default_limiter(limiter):
|
|
289
|
+
"""Set the default global resource limiter"""
|
|
290
|
+
global _default_limiter
|
|
291
|
+
_default_limiter = limiter
|
|
@@ -757,15 +757,23 @@ class StatementEvaluatorMixin:
|
|
|
757
757
|
if is_error(obj):
|
|
758
758
|
return obj
|
|
759
759
|
|
|
760
|
-
#
|
|
761
|
-
if hasattr(node.name
|
|
762
|
-
|
|
763
|
-
else:
|
|
764
|
-
# Evaluate property expression
|
|
760
|
+
# Determine property key based on whether it's computed (obj[expr]) or literal (obj.prop)
|
|
761
|
+
if hasattr(node.name, 'computed') and node.name.computed:
|
|
762
|
+
# Computed property (obj[expr]) - evaluate the expression
|
|
765
763
|
prop_result = self.eval_node(node.name.property, env, stack_trace)
|
|
766
764
|
if is_error(prop_result):
|
|
767
765
|
return prop_result
|
|
768
766
|
prop_key = prop_result.value if hasattr(prop_result, 'value') else str(prop_result)
|
|
767
|
+
else:
|
|
768
|
+
# Literal property (obj.prop) - use the identifier name directly
|
|
769
|
+
if hasattr(node.name.property, 'value'):
|
|
770
|
+
prop_key = node.name.property.value
|
|
771
|
+
else:
|
|
772
|
+
# Fallback: evaluate it
|
|
773
|
+
prop_result = self.eval_node(node.name.property, env, stack_trace)
|
|
774
|
+
if is_error(prop_result):
|
|
775
|
+
return prop_result
|
|
776
|
+
prop_key = prop_result.value if hasattr(prop_result, 'value') else str(prop_result)
|
|
769
777
|
|
|
770
778
|
# Evaluate value first
|
|
771
779
|
value = self.eval_node(node.value, env, stack_trace)
|
|
@@ -877,6 +885,16 @@ class StatementEvaluatorMixin:
|
|
|
877
885
|
def eval_while_statement(self, node, env, stack_trace):
|
|
878
886
|
result = NULL
|
|
879
887
|
while True:
|
|
888
|
+
# Resource limit check (Security Fix #7)
|
|
889
|
+
try:
|
|
890
|
+
self.resource_limiter.check_iterations()
|
|
891
|
+
except Exception as e:
|
|
892
|
+
# Convert ResourceError to EvaluationError
|
|
893
|
+
from ..evaluator.resource_limiter import ResourceError, TimeoutError
|
|
894
|
+
if isinstance(e, (ResourceError, TimeoutError)):
|
|
895
|
+
return EvaluationError(str(e))
|
|
896
|
+
raise # Re-raise if not a resource error
|
|
897
|
+
|
|
880
898
|
cond = self.eval_node(node.condition, env, stack_trace)
|
|
881
899
|
if is_error(cond):
|
|
882
900
|
return cond
|
|
@@ -904,6 +922,16 @@ class StatementEvaluatorMixin:
|
|
|
904
922
|
|
|
905
923
|
result = NULL
|
|
906
924
|
for item in iterable.elements:
|
|
925
|
+
# Resource limit check (Security Fix #7)
|
|
926
|
+
try:
|
|
927
|
+
self.resource_limiter.check_iterations()
|
|
928
|
+
except Exception as e:
|
|
929
|
+
# Convert ResourceError to EvaluationError
|
|
930
|
+
from ..evaluator.resource_limiter import ResourceError, TimeoutError
|
|
931
|
+
if isinstance(e, (ResourceError, TimeoutError)):
|
|
932
|
+
return EvaluationError(str(e))
|
|
933
|
+
raise # Re-raise if not a resource error
|
|
934
|
+
|
|
907
935
|
env.set(node.item.value, item)
|
|
908
936
|
result = self.eval_node(node.body, env, stack_trace)
|
|
909
937
|
if isinstance(result, ReturnValue):
|
|
@@ -1688,11 +1716,10 @@ class StatementEvaluatorMixin:
|
|
|
1688
1716
|
|
|
1689
1717
|
# Pass the AST nodes as storage_vars, not the storage dict
|
|
1690
1718
|
contract = SmartContract(node.name.value, node.storage_vars, actions)
|
|
1691
|
-
|
|
1719
|
+
# Deploy with evaluated storage values to avoid storing AST nodes
|
|
1720
|
+
contract.deploy(evaluated_storage_values=storage)
|
|
1692
1721
|
|
|
1693
|
-
#
|
|
1694
|
-
for var_name, init_val in storage.items():
|
|
1695
|
-
contract.storage.set(var_name, init_val)
|
|
1722
|
+
# Storage values are now set during deploy(), no need to set again
|
|
1696
1723
|
|
|
1697
1724
|
# Check if contract has a constructor and execute it
|
|
1698
1725
|
if 'constructor' in actions:
|
|
@@ -3115,12 +3142,14 @@ class StatementEvaluatorMixin:
|
|
|
3115
3142
|
else:
|
|
3116
3143
|
data_str = str(data)
|
|
3117
3144
|
|
|
3118
|
-
# Determine encoding
|
|
3145
|
+
# Determine encoding/context
|
|
3119
3146
|
encoding = Encoding.HTML # Default
|
|
3147
|
+
context_name = "html" # For marking sanitized_for
|
|
3120
3148
|
if node.encoding:
|
|
3121
3149
|
enc_val = self.eval_node(node.encoding, env, stack_trace)
|
|
3122
3150
|
if hasattr(enc_val, 'value'):
|
|
3123
3151
|
enc_name = enc_val.value.upper()
|
|
3152
|
+
context_name = enc_val.value.lower()
|
|
3124
3153
|
try:
|
|
3125
3154
|
encoding = Encoding[enc_name]
|
|
3126
3155
|
except KeyError:
|
|
@@ -3130,10 +3159,16 @@ class StatementEvaluatorMixin:
|
|
|
3130
3159
|
try:
|
|
3131
3160
|
sanitized = Sanitizer.sanitize_string(data_str, encoding)
|
|
3132
3161
|
debug_log("eval_sanitize_statement", f"Sanitized {len(data_str)} chars with {encoding.value}")
|
|
3133
|
-
|
|
3162
|
+
result = String(sanitized)
|
|
3163
|
+
# SECURITY ENFORCEMENT: Mark as sanitized for this context
|
|
3164
|
+
result.mark_sanitized(context_name)
|
|
3165
|
+
return result
|
|
3134
3166
|
except Exception as e:
|
|
3135
3167
|
debug_log("eval_sanitize_statement", f"Sanitization error: {e}")
|
|
3136
|
-
|
|
3168
|
+
# Even on error, mark as sanitized to prevent double-sanitization loops
|
|
3169
|
+
result = String(data_str)
|
|
3170
|
+
result.mark_sanitized(context_name)
|
|
3171
|
+
return result
|
|
3137
3172
|
|
|
3138
3173
|
def eval_inject_statement(self, node, env, stack_trace):
|
|
3139
3174
|
"""Evaluate inject statement - full dependency injection with mode-aware resolution."""
|
|
@@ -89,20 +89,26 @@ def _zexus_to_python(value):
|
|
|
89
89
|
else:
|
|
90
90
|
return str(value)
|
|
91
91
|
|
|
92
|
-
def _python_to_zexus(value):
|
|
93
|
-
"""Convert Python native types to Zexus objects
|
|
92
|
+
def _python_to_zexus(value, mark_untrusted=False):
|
|
93
|
+
"""Convert Python native types to Zexus objects
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
value: Python value to convert
|
|
97
|
+
mark_untrusted: If True, mark strings as untrusted (external data)
|
|
98
|
+
"""
|
|
94
99
|
from ..object import Map, List, String, Integer, Float, Boolean as BooleanObj
|
|
95
100
|
|
|
96
101
|
if isinstance(value, dict):
|
|
97
102
|
pairs = {}
|
|
98
103
|
for k, v in value.items():
|
|
99
|
-
pairs[k] = _python_to_zexus(v)
|
|
104
|
+
pairs[k] = _python_to_zexus(v, mark_untrusted)
|
|
100
105
|
return Map(pairs)
|
|
101
106
|
elif isinstance(value, list):
|
|
102
|
-
zexus_list = List([_python_to_zexus(item) for item in value])
|
|
107
|
+
zexus_list = List([_python_to_zexus(item, mark_untrusted) for item in value])
|
|
103
108
|
return zexus_list
|
|
104
109
|
elif isinstance(value, str):
|
|
105
|
-
|
|
110
|
+
# Mark strings as untrusted if from external source (HTTP, DB, etc.)
|
|
111
|
+
return String(value, is_trusted=not mark_untrusted)
|
|
106
112
|
elif isinstance(value, int):
|
|
107
113
|
return Integer(value)
|
|
108
114
|
elif isinstance(value, float):
|
|
@@ -112,7 +118,7 @@ def _python_to_zexus(value):
|
|
|
112
118
|
elif value is None:
|
|
113
119
|
return NULL
|
|
114
120
|
else:
|
|
115
|
-
return String(str(value))
|
|
121
|
+
return String(str(value), is_trusted=not mark_untrusted)
|
|
116
122
|
|
|
117
123
|
def _to_str(obj):
|
|
118
124
|
"""Helper to convert Zexus object to string"""
|
package/src/zexus/lsp/server.py
CHANGED
|
@@ -87,7 +87,7 @@ if PYGLS_AVAILABLE:
|
|
|
87
87
|
"""Zexus Language Server implementation."""
|
|
88
88
|
|
|
89
89
|
def __init__(self):
|
|
90
|
-
super().__init__('zexus-language-server', 'v1.6.
|
|
90
|
+
super().__init__('zexus-language-server', 'v1.6.3')
|
|
91
91
|
self.completion_provider = CompletionProvider()
|
|
92
92
|
self.symbol_provider = SymbolProvider()
|
|
93
93
|
self.hover_provider = HoverProvider()
|
package/src/zexus/object.py
CHANGED
|
@@ -31,7 +31,12 @@ class Null(Object):
|
|
|
31
31
|
def type(self): return "NULL"
|
|
32
32
|
|
|
33
33
|
class String(Object):
|
|
34
|
-
def __init__(self, value
|
|
34
|
+
def __init__(self, value, sanitized_for=None, is_trusted=False):
|
|
35
|
+
self.value = value
|
|
36
|
+
# Track sanitization status for security enforcement
|
|
37
|
+
self.sanitized_for = sanitized_for # None, 'sql', 'html', 'url', 'shell', etc.
|
|
38
|
+
self.is_trusted = is_trusted # True for literals, False for external input
|
|
39
|
+
|
|
35
40
|
def inspect(self): return self.value
|
|
36
41
|
def type(self): return "STRING"
|
|
37
42
|
def __str__(self): return self.value
|
|
@@ -43,6 +48,19 @@ class String(Object):
|
|
|
43
48
|
def __hash__(self):
|
|
44
49
|
"""Enable String objects to be used as dict keys"""
|
|
45
50
|
return hash(self.value)
|
|
51
|
+
|
|
52
|
+
def mark_sanitized(self, context):
|
|
53
|
+
"""Mark this string as sanitized for a specific context"""
|
|
54
|
+
self.sanitized_for = context
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def is_safe_for(self, context):
|
|
58
|
+
"""Check if string is safe to use in given context"""
|
|
59
|
+
# Trusted strings (literals) are always safe
|
|
60
|
+
if self.is_trusted:
|
|
61
|
+
return True
|
|
62
|
+
# Check if sanitized for this specific context
|
|
63
|
+
return self.sanitized_for == context
|
|
46
64
|
|
|
47
65
|
class List(Object):
|
|
48
66
|
def __init__(self, elements): self.elements = elements
|
|
@@ -425,7 +443,8 @@ class File(Object):
|
|
|
425
443
|
if isinstance(path, String):
|
|
426
444
|
path = path.value
|
|
427
445
|
with open(path, 'r', encoding='utf-8') as f:
|
|
428
|
-
return
|
|
446
|
+
# Files are external data sources - return untrusted strings
|
|
447
|
+
return String(f.read(), is_trusted=False)
|
|
429
448
|
except Exception as e:
|
|
430
449
|
return EvaluationError(f"File read error: {str(e)}")
|
|
431
450
|
|
|
@@ -27,6 +27,7 @@ precedences = {
|
|
|
27
27
|
SLASH: PRODUCT, STAR: PRODUCT, MOD: PRODUCT,
|
|
28
28
|
LPAREN: CALL,
|
|
29
29
|
LBRACKET: CALL,
|
|
30
|
+
LBRACE: CALL, # Entity{...} constructor syntax
|
|
30
31
|
DOT: CALL,
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -82,6 +83,7 @@ class UltimateParser:
|
|
|
82
83
|
TRY: self.parse_try_catch_statement,
|
|
83
84
|
EXTERNAL: self.parse_external_declaration,
|
|
84
85
|
ASYNC: self.parse_async_expression, # Support async <expression>
|
|
86
|
+
SANITIZE: self.parse_sanitize_expression, # FIX #4: Support sanitize as expression
|
|
85
87
|
}
|
|
86
88
|
self.infix_parse_fns = {
|
|
87
89
|
PLUS: self.parse_infix_expression,
|
|
@@ -102,6 +104,7 @@ class UltimateParser:
|
|
|
102
104
|
ASSIGN: self.parse_assignment_expression,
|
|
103
105
|
LAMBDA: self.parse_lambda_infix, # support arrow-style lambdas: params => body
|
|
104
106
|
LPAREN: self.parse_call_expression,
|
|
107
|
+
LBRACE: self.parse_constructor_call_expression, # Entity{field: value} syntax
|
|
105
108
|
LBRACKET: self.parse_index_expression,
|
|
106
109
|
DOT: self.parse_method_call_expression,
|
|
107
110
|
}
|
|
@@ -511,7 +514,8 @@ class UltimateParser:
|
|
|
511
514
|
elif self.cur_token_is(STATE):
|
|
512
515
|
print(f"[PARSE_STMT] Matched STATE", file=sys.stderr, flush=True)
|
|
513
516
|
node = self.parse_state_statement()
|
|
514
|
-
|
|
517
|
+
elif self.cur_token_is(REQUIRE):
|
|
518
|
+
node = self.parse_require_statement()
|
|
515
519
|
elif self.cur_token_is(REVERT):
|
|
516
520
|
print(f"[PARSE_STMT] Matched REVERT", file=sys.stderr, flush=True)
|
|
517
521
|
node = self.parse_revert_statement()
|
|
@@ -1504,7 +1508,7 @@ class UltimateParser:
|
|
|
1504
1508
|
arguments = self.parse_expression_list(RPAREN)
|
|
1505
1509
|
return MethodCallExpression(object=left, method=method, arguments=arguments)
|
|
1506
1510
|
else:
|
|
1507
|
-
return PropertyAccessExpression(object=left, property=method)
|
|
1511
|
+
return PropertyAccessExpression(object=left, property=method, computed=False)
|
|
1508
1512
|
|
|
1509
1513
|
def parse_export_statement(self):
|
|
1510
1514
|
token = self.cur_token
|
|
@@ -1707,7 +1711,7 @@ class UltimateParser:
|
|
|
1707
1711
|
return None
|
|
1708
1712
|
|
|
1709
1713
|
field_name = Identifier(self.cur_token.literal)
|
|
1710
|
-
target = PropertyAccessExpression(obj_name, field_name)
|
|
1714
|
+
target = PropertyAccessExpression(obj_name, field_name, computed=False)
|
|
1711
1715
|
|
|
1712
1716
|
# Expect assignment
|
|
1713
1717
|
if not self.expect_peek(ASSIGN):
|
|
@@ -2726,6 +2730,18 @@ class UltimateParser:
|
|
|
2726
2730
|
exp.arguments = self.parse_expression_list(RPAREN)
|
|
2727
2731
|
return exp
|
|
2728
2732
|
|
|
2733
|
+
def parse_constructor_call_expression(self, function):
|
|
2734
|
+
"""Parse constructor call with map literal syntax: Entity{field: value, ...}
|
|
2735
|
+
|
|
2736
|
+
This converts Entity{a: 1, b: 2} into Entity({a: 1, b: 2})
|
|
2737
|
+
"""
|
|
2738
|
+
# Current token is LBRACE, parse it as a map literal
|
|
2739
|
+
map_literal = self.parse_map_literal()
|
|
2740
|
+
|
|
2741
|
+
# Create a call expression with the map as the single argument
|
|
2742
|
+
exp = CallExpression(function=function, arguments=[map_literal])
|
|
2743
|
+
return exp
|
|
2744
|
+
|
|
2729
2745
|
def parse_prefix_expression(self):
|
|
2730
2746
|
expression = PrefixExpression(operator=self.cur_token.literal, right=None)
|
|
2731
2747
|
self.next_token()
|
|
@@ -2798,7 +2814,7 @@ class UltimateParser:
|
|
|
2798
2814
|
# Expect closing bracket
|
|
2799
2815
|
if not self.expect_peek(RBRACKET):
|
|
2800
2816
|
return None
|
|
2801
|
-
return PropertyAccessExpression(object=left, property=index_expr)
|
|
2817
|
+
return PropertyAccessExpression(object=left, property=index_expr, computed=True)
|
|
2802
2818
|
|
|
2803
2819
|
def _lookahead_token_after_matching_paren(self):
|
|
2804
2820
|
"""Character-level lookahead: detect if the matching ')' is followed by '=>' (arrow).
|
|
@@ -3299,6 +3315,40 @@ class UltimateParser:
|
|
|
3299
3315
|
|
|
3300
3316
|
return ValidateStatement(data=data_expr, schema=schema_expr)
|
|
3301
3317
|
|
|
3318
|
+
def parse_sanitize_expression(self):
|
|
3319
|
+
"""Parse sanitize as expression - can be used in assignments
|
|
3320
|
+
|
|
3321
|
+
Supports both:
|
|
3322
|
+
let safe = sanitize data, "sql"
|
|
3323
|
+
let safe = sanitize data as sql
|
|
3324
|
+
"""
|
|
3325
|
+
token = self.cur_token
|
|
3326
|
+
self.next_token()
|
|
3327
|
+
|
|
3328
|
+
# Parse data expression
|
|
3329
|
+
data_expr = self.parse_expression(LOWEST)
|
|
3330
|
+
if data_expr is None:
|
|
3331
|
+
self.errors.append(f"Line {token.line}:{token.column} - Expected expression to sanitize")
|
|
3332
|
+
return None
|
|
3333
|
+
|
|
3334
|
+
# Expect comma or 'as'
|
|
3335
|
+
encoding = None
|
|
3336
|
+
if self.cur_token_is(COMMA):
|
|
3337
|
+
self.next_token()
|
|
3338
|
+
# Parse encoding as expression (can be string literal or identifier)
|
|
3339
|
+
encoding = self.parse_expression(LOWEST)
|
|
3340
|
+
elif self.cur_token_is(IDENT) and self.cur_token.literal == 'as':
|
|
3341
|
+
self.next_token()
|
|
3342
|
+
if self.cur_token_is(IDENT):
|
|
3343
|
+
# Convert identifier to string literal
|
|
3344
|
+
encoding = StringLiteral(value=self.cur_token.literal)
|
|
3345
|
+
self.next_token()
|
|
3346
|
+
elif self.cur_token_is(STRING):
|
|
3347
|
+
encoding = self.parse_string_literal()
|
|
3348
|
+
|
|
3349
|
+
result = SanitizeStatement(data=data_expr, rules=None, encoding=encoding)
|
|
3350
|
+
return result
|
|
3351
|
+
|
|
3302
3352
|
def parse_sanitize_statement(self):
|
|
3303
3353
|
"""Parse sanitize statement - sanitize data"""
|
|
3304
3354
|
token = self.cur_token
|
|
@@ -3698,6 +3748,7 @@ class UltimateParser:
|
|
|
3698
3748
|
|
|
3699
3749
|
Asserts condition, reverts transaction if false.
|
|
3700
3750
|
"""
|
|
3751
|
+
print(f"[DEBUG PARSER] parse_require_statement called", flush=True)
|
|
3701
3752
|
token = self.cur_token
|
|
3702
3753
|
|
|
3703
3754
|
if not self.expect_peek(LPAREN):
|
|
@@ -3721,6 +3772,7 @@ class UltimateParser:
|
|
|
3721
3772
|
if self.peek_token_is(SEMICOLON):
|
|
3722
3773
|
self.next_token()
|
|
3723
3774
|
|
|
3775
|
+
print(f"[DEBUG PARSER] Creating RequireStatement with condition={condition}, message={message}", flush=True)
|
|
3724
3776
|
return RequireStatement(condition=condition, message=message)
|
|
3725
3777
|
|
|
3726
3778
|
def parse_revert_statement(self):
|