• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / RestrictedPython / 18631514313

19 Oct 2025 02:05PM UTC coverage: 97.971% (-0.8%) from 98.772%
18631514313

Pull #304

github

dataflake
- fix test code
Pull Request #304: Add Python 3.14 support

214 of 233 branches covered (91.85%)

18 of 39 new or added lines in 3 files covered. (46.15%)

2511 of 2563 relevant lines covered (97.97%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.99
/src/RestrictedPython/transformer.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE
11
#
12
##############################################################################
13
"""
14
transformer module:
15

16
uses Python standard library ast module and its containing classes to transform
17
the parsed python code to create a modified AST for a byte code generation.
18
"""
19

20

21
import ast
1✔
22
import contextlib
1✔
23
import textwrap
1✔
24

25

26
# For AugAssign the operator must be converted to a string.
27
IOPERATOR_TO_STR = {
1✔
28
    ast.Add: '+=',
29
    ast.Sub: '-=',
30
    ast.Mult: '*=',
31
    ast.Div: '/=',
32
    ast.Mod: '%=',
33
    ast.Pow: '**=',
34
    ast.LShift: '<<=',
35
    ast.RShift: '>>=',
36
    ast.BitOr: '|=',
37
    ast.BitXor: '^=',
38
    ast.BitAnd: '&=',
39
    ast.FloorDiv: '//=',
40
    ast.MatMult: '@=',
41
}
42

43
# For creation allowed magic method names. See also
44
# https://docs.python.org/3/reference/datamodel.html#special-method-names
45
ALLOWED_FUNC_NAMES = frozenset([
1✔
46
    '__init__',
47
    '__contains__',
48
    '__lt__',
49
    '__le__',
50
    '__eq__',
51
    '__ne__',
52
    '__gt__',
53
    '__ge__',
54
])
55

56

57
FORBIDDEN_FUNC_NAMES = frozenset([
1✔
58
    'print',
59
    'printed',
60
    'builtins',
61
    'breakpoint',
62
])
63

64
# Attributes documented in the `inspect` module, but defined on the listed
65
# objects. See also https://docs.python.org/3/library/inspect.html
66
INSPECT_ATTRIBUTES = frozenset([
1✔
67
    # on traceback objects:
68
    "tb_frame",
69
    # "tb_lasti",  # int
70
    # "tb_lineno",  # int
71
    "tb_next",
72
    # on frame objects:
73
    "f_back",
74
    "f_builtins",
75
    "f_code",
76
    "f_generator",
77
    "f_globals",
78
    # "f_lasti",  # int
79
    # "f_lineno",  # int
80
    "f_locals",
81
    "f_trace",
82
    # on code objects:
83
    # "co_argcount",  # int
84
    "co_code",
85
    # "co_cellvars",  # tuple of str
86
    # "co_consts",   # tuple of str
87
    # "co_filename",  # str
88
    # "co_firstlineno",  # int
89
    # "co_flags",  # int
90
    # "co_lnotab",  # mapping between ints and indices
91
    # "co_freevars",  # tuple of strings
92
    # "co_posonlyargcount",  # int
93
    # "co_kwonlyargcount",  # int
94
    # "co_name",  # str
95
    # "co_qualname",  # str
96
    # "co_names",  # str
97
    # "co_nlocals",  # int
98
    # "co_stacksize",  # int
99
    # "co_varnames",  # tuple of str
100
    # on generator objects:
101
    "gi_frame",
102
    # "gi_running",  # bool
103
    # "gi_suspended",  # bool
104
    "gi_code",
105
    "gi_yieldfrom",
106
    # on coroutine objects:
107
    "cr_await",
108
    "cr_frame",
109
    # "cr_running",  # bool
110
    "cr_code",
111
    "cr_origin",
112
])
113

114

115
# When new ast nodes are generated they have no 'lineno', 'end_lineno',
116
# 'col_offset' and 'end_col_offset'. This function copies these fields from the
117
# incoming node:
118
def copy_locations(new_node, old_node):
1✔
119
    assert 'lineno' in new_node._attributes
1✔
120
    new_node.lineno = old_node.lineno
1✔
121

122
    assert 'end_lineno' in new_node._attributes
1✔
123
    new_node.end_lineno = old_node.end_lineno
1✔
124

125
    assert 'col_offset' in new_node._attributes
1✔
126
    new_node.col_offset = old_node.col_offset
1✔
127

128
    assert 'end_col_offset' in new_node._attributes
1✔
129
    new_node.end_col_offset = old_node.end_col_offset
1✔
130

131
    ast.fix_missing_locations(new_node)
1✔
132

133

134
class PrintInfo:
1✔
135
    def __init__(self):
1✔
136
        self.print_used = False
1✔
137
        self.printed_used = False
1✔
138

139
    @contextlib.contextmanager
1✔
140
    def new_print_scope(self):
1✔
141
        old_print_used = self.print_used
1✔
142
        old_printed_used = self.printed_used
1✔
143

144
        self.print_used = False
1✔
145
        self.printed_used = False
1✔
146

147
        try:
1✔
148
            yield
1✔
149
        finally:
150
            self.print_used = old_print_used
1✔
151
            self.printed_used = old_printed_used
1✔
152

153

154
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
155

156
    def __init__(self, errors=None, warnings=None, used_names=None):
1✔
157
        super().__init__()
1✔
158
        self.errors = [] if errors is None else errors
1✔
159
        self.warnings = [] if warnings is None else warnings
1✔
160

161
        # All the variables used by the incoming source.
162
        # Internal names/variables, like the ones from 'gen_tmp_name', don't
163
        # have to be added.
164
        # 'used_names' is for example needed by 'RestrictionCapableEval' to
165
        # know wich names it has to supply when calling the final code.
166
        self.used_names = {} if used_names is None else used_names
1✔
167

168
        # Global counter to construct temporary variable names.
169
        self._tmp_idx = 0
1✔
170

171
        self.print_info = PrintInfo()
1✔
172

173
    def gen_tmp_name(self):
1✔
174
        # 'check_name' ensures that no variable is prefixed with '_'.
175
        # => Its safe to use '_tmp..' as a temporary variable.
176
        name = '_tmp%i' % self._tmp_idx
1✔
177
        self._tmp_idx += 1
1✔
178
        return name
1✔
179

180
    def error(self, node, info):
1✔
181
        """Record a security error discovered during transformation."""
182
        lineno = getattr(node, 'lineno', None)
1✔
183
        self.errors.append(
1✔
184
            f'Line {lineno}: {info}')
185

186
    def warn(self, node, info):
1✔
187
        """Record a security error discovered during transformation."""
188
        lineno = getattr(node, 'lineno', None)
1✔
189
        self.warnings.append(
1✔
190
            f'Line {lineno}: {info}')
191

192
    def guard_iter(self, node):
1✔
193
        """
194
        Converts:
195
            for x in expr
196
        to
197
            for x in _getiter_(expr)
198

199
        Also used for
200
        * list comprehensions
201
        * dict comprehensions
202
        * set comprehensions
203
        * generator expresions
204
        """
205
        node = self.node_contents_visit(node)
1✔
206

207
        if isinstance(node.target, ast.Tuple):
1✔
208
            spec = self.gen_unpack_spec(node.target)
1✔
209
            new_iter = ast.Call(
1✔
210
                func=ast.Name('_iter_unpack_sequence_', ast.Load()),
211
                args=[node.iter, spec, ast.Name('_getiter_', ast.Load())],
212
                keywords=[])
213
        else:
214
            new_iter = ast.Call(
1✔
215
                func=ast.Name("_getiter_", ast.Load()),
216
                args=[node.iter],
217
                keywords=[])
218

219
        copy_locations(new_iter, node.iter)
1✔
220
        node.iter = new_iter
1✔
221
        return node
1✔
222

223
    def is_starred(self, ob):
1✔
224
        return isinstance(ob, ast.Starred)
1✔
225

226
    def gen_unpack_spec(self, tpl):
1✔
227
        """Generate a specification for 'guarded_unpack_sequence'.
228

229
        This spec is used to protect sequence unpacking.
230
        The primary goal of this spec is to tell which elements in a sequence
231
        are sequences again. These 'child' sequences have to be protected
232
        again.
233

234
        For example there is a sequence like this:
235
            (a, (b, c), (d, (e, f))) = g
236

237
        On a higher level the spec says:
238
            - There is a sequence of len 3
239
            - The element at index 1 is a sequence again with len 2
240
            - The element at index 2 is a sequence again with len 2
241
              - The element at index 1 in this subsequence is a sequence again
242
                with len 2
243

244
        With this spec 'guarded_unpack_sequence' does something like this for
245
        protection (len checks are omitted):
246

247
            t = list(_getiter_(g))
248
            t[1] = list(_getiter_(t[1]))
249
            t[2] = list(_getiter_(t[2]))
250
            t[2][1] = list(_getiter_(t[2][1]))
251
            return t
252

253
        The 'real' spec for the case above is then:
254
            spec = {
255
                'min_len': 3,
256
                'childs': (
257
                    (1, {'min_len': 2, 'childs': ()}),
258
                    (2, {
259
                            'min_len': 2,
260
                            'childs': (
261
                                (1, {'min_len': 2, 'childs': ()})
262
                            )
263
                        }
264
                    )
265
                )
266
            }
267

268
        So finally the assignment above is converted into:
269
            (a, (b, c), (d, (e, f))) = guarded_unpack_sequence(g, spec)
270
        """
271
        spec = ast.Dict(keys=[], values=[])
1✔
272

273
        spec.keys.append(ast.Constant('childs'))
1✔
274
        spec.values.append(ast.Tuple([], ast.Load()))
1✔
275

276
        # starred elements in a sequence do not contribute into the min_len.
277
        # For example a, b, *c = g
278
        # g must have at least 2 elements, not 3. 'c' is empyt if g has only 2.
279
        min_len = len([ob for ob in tpl.elts if not self.is_starred(ob)])
1✔
280
        offset = 0
1✔
281

282
        for idx, val in enumerate(tpl.elts):
1✔
283
            # After a starred element specify the child index from the back.
284
            # Since it is unknown how many elements from the sequence are
285
            # consumed by the starred element.
286
            # For example a, *b, (c, d) = g
287
            # Then (c, d) has the index '-1'
288
            if self.is_starred(val):
1✔
289
                offset = min_len + 1
1✔
290

291
            elif isinstance(val, ast.Tuple):
1✔
292
                el = ast.Tuple([], ast.Load())
1✔
293
                el.elts.append(ast.Constant(idx - offset))
1✔
294
                el.elts.append(self.gen_unpack_spec(val))
1✔
295
                spec.values[0].elts.append(el)
1✔
296

297
        spec.keys.append(ast.Constant('min_len'))
1✔
298
        spec.values.append(ast.Constant(min_len))
1✔
299

300
        return spec
1✔
301

302
    def protect_unpack_sequence(self, target, value):
1✔
303
        spec = self.gen_unpack_spec(target)
1✔
304
        return ast.Call(
1✔
305
            func=ast.Name('_unpack_sequence_', ast.Load()),
306
            args=[value, spec, ast.Name('_getiter_', ast.Load())],
307
            keywords=[])
308

309
    def gen_unpack_wrapper(self, node, target):
1✔
310
        """Helper function to protect tuple unpacks.
311

312
        node: used to copy the locations for the new nodes.
313
        target: is the tuple which must be protected.
314

315
        It returns a tuple with two element.
316

317
        Element 1: Is a temporary name node which must be used to
318
                   replace the target.
319
                   The context (store, param) is defined
320
                   by the 'ctx' parameter..
321

322
        Element 2: Is a try .. finally where the body performs the
323
                   protected tuple unpack of the temporary variable
324
                   into the original target.
325
        """
326

327
        # Generate a tmp name to replace the tuple with.
328
        tmp_name = self.gen_tmp_name()
1✔
329

330
        # Generates an expressions which protects the unpack.
331
        # converter looks like 'wrapper(tmp_name)'.
332
        # 'wrapper' takes care to protect sequence unpacking with _getiter_.
333
        converter = self.protect_unpack_sequence(
1✔
334
            target,
335
            ast.Name(tmp_name, ast.Load()))
336

337
        # Assign the expression to the original names.
338
        # Cleanup the temporary variable.
339
        # Generates:
340
        # try:
341
        #     # converter is 'wrapper(tmp_name)'
342
        #     arg = converter
343
        # finally:
344
        #     del tmp_arg
345
        try_body = [ast.Assign(targets=[target], value=converter)]
1✔
346
        finalbody = [self.gen_del_stmt(tmp_name)]
1✔
347
        cleanup = ast.Try(
1✔
348
            body=try_body, finalbody=finalbody, handlers=[], orelse=[])
349

350
        # This node is used to catch the tuple in a tmp variable.
351
        tmp_target = ast.Name(tmp_name, ast.Store())
1✔
352

353
        copy_locations(tmp_target, node)
1✔
354
        copy_locations(cleanup, node)
1✔
355

356
        return (tmp_target, cleanup)
1✔
357

358
    def gen_none_node(self):
1✔
359
        return ast.NameConstant(value=None)
×
360

361
    def gen_del_stmt(self, name_to_del):
1✔
362
        return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())])
1✔
363

364
    def transform_slice(self, slice_):
1✔
365
        """Transform slices into function parameters.
366

367
        ast.Slice nodes are only allowed within a ast.Subscript node.
368
        To use a slice as an argument of ast.Call it has to be converted.
369
        Conversion is done by calling the 'slice' function from builtins
370
        """
371

372
        if isinstance(slice_, ast.expr):
1!
373
            # Python 3.9+
374
            return slice_
1✔
375

376
        elif isinstance(slice_, ast.Index):
×
377
            return slice_.value
×
378

379
        elif isinstance(slice_, ast.Slice):
×
380
            # Create a python slice object.
381
            args = []
×
382

383
            if slice_.lower:
×
384
                args.append(slice_.lower)
×
385
            else:
386
                args.append(self.gen_none_node())
×
387

388
            if slice_.upper:
×
389
                args.append(slice_.upper)
×
390
            else:
391
                args.append(self.gen_none_node())
×
392

393
            if slice_.step:
×
394
                args.append(slice_.step)
×
395
            else:
396
                args.append(self.gen_none_node())
×
397

398
            return ast.Call(
×
399
                func=ast.Name('slice', ast.Load()),
400
                args=args,
401
                keywords=[])
402

403
        elif isinstance(slice_, ast.ExtSlice):
×
404
            dims = ast.Tuple([], ast.Load())
×
405
            for item in slice_.dims:
×
406
                dims.elts.append(self.transform_slice(item))
×
407
            return dims
×
408

409
        else:  # pragma: no cover
410
            # Index, Slice and ExtSlice are only defined Slice types.
411
            raise NotImplementedError(f"Unknown slice type: {slice_}")
412

413
    def check_name(self, node, name, allow_magic_methods=False):
1✔
414
        """Check names if they are allowed.
415

416
        If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES`
417
        are additionally allowed although their names start with `_`.
418

419
        """
420
        if name is None:
1✔
421
            return
1✔
422

423
        if (name.startswith('_')
1✔
424
                and name != '_'
425
                and not (allow_magic_methods
426
                         and name in ALLOWED_FUNC_NAMES
427
                         and node.col_offset != 0)):
428
            self.error(
1✔
429
                node,
430
                '"{name}" is an invalid variable name because it '
431
                'starts with "_"'.format(name=name))
432
        elif name.endswith('__roles__'):
1✔
433
            self.error(node, '"%s" is an invalid variable name because '
1✔
434
                       'it ends with "__roles__".' % name)
435
        elif name in FORBIDDEN_FUNC_NAMES:
1✔
436
            self.error(node, f'"{name}" is a reserved name.')
1✔
437

438
    def check_function_argument_names(self, node):
1✔
439
        for arg in node.args.args:
1✔
440
            self.check_name(node, arg.arg)
1✔
441

442
        if node.args.vararg:
1✔
443
            self.check_name(node, node.args.vararg.arg)
1✔
444

445
        if node.args.kwarg:
1✔
446
            self.check_name(node, node.args.kwarg.arg)
1✔
447

448
        for arg in node.args.kwonlyargs:
1✔
449
            self.check_name(node, arg.arg)
1✔
450

451
    def check_import_names(self, node):
1✔
452
        """Check the names being imported.
453

454
        This is a protection against rebinding dunder names like
455
        _getitem_, _write_ via imports.
456

457
        => 'from _a import x' is ok, because '_a' is not added to the scope.
458
        """
459
        for name in node.names:
1✔
460
            if '*' in name.name:
1✔
461
                self.error(node, '"*" imports are not allowed.')
1✔
462
            self.check_name(node, name.name)
1✔
463
            if name.asname:
1✔
464
                self.check_name(node, name.asname)
1✔
465

466
        return self.node_contents_visit(node)
1✔
467

468
    def inject_print_collector(self, node, position=0):
1✔
469
        print_used = self.print_info.print_used
1✔
470
        printed_used = self.print_info.printed_used
1✔
471

472
        if print_used or printed_used:
1✔
473
            # Add '_print = _print_(_getattr_)' add the top of a
474
            # function/module.
475
            _print = ast.Assign(
1✔
476
                targets=[ast.Name('_print', ast.Store())],
477
                value=ast.Call(
478
                    func=ast.Name("_print_", ast.Load()),
479
                    args=[ast.Name("_getattr_", ast.Load())],
480
                    keywords=[]))
481

482
            if isinstance(node, ast.Module):
1✔
483
                _print.lineno = position
1✔
484
                _print.col_offset = position
1✔
485
                _print.end_lineno = position
1✔
486
                _print.end_col_offset = position
1✔
487
                ast.fix_missing_locations(_print)
1✔
488
            else:
489
                copy_locations(_print, node)
1✔
490

491
            node.body.insert(position, _print)
1✔
492

493
            if not printed_used:
1✔
494
                self.warn(node, "Prints, but never reads 'printed' variable.")
1✔
495

496
            elif not print_used:
1✔
497
                self.warn(node, "Doesn't print, but reads 'printed' variable.")
1✔
498

499
    # Special Functions for an ast.NodeTransformer
500

501
    def generic_visit(self, node):
1✔
502
        """Reject ast nodes which do not have a corresponding `visit_` method.
503

504
        This is needed to prevent new ast nodes from new Python versions to be
505
        trusted before any security review.
506

507
        To access `generic_visit` on the super class use `node_contents_visit`.
508
        """
509
        self.warn(
1✔
510
            node,
511
            '{0.__class__.__name__}'
512
            ' statement is not known to RestrictedPython'.format(node)
513
        )
514
        self.not_allowed(node)
1✔
515

516
    def not_allowed(self, node):
1✔
517
        self.error(
1✔
518
            node,
519
            f'{node.__class__.__name__} statements are not allowed.')
520

521
    def node_contents_visit(self, node):
1✔
522
        """Visit the contents of a node."""
523
        return super().generic_visit(node)
1✔
524

525
    # ast for Literals
526

527
    def visit_Constant(self, node):
1✔
528
        """Allow constant literals with restriction for Ellipsis.
529

530
        Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in
531
        Python 3.8+.
532
        :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated
533
        """
534
        if node.value is Ellipsis:
1✔
535
            # Deny using `...`.
536
            # Special handling necessary as ``self.not_allowed(node)``
537
            # would return the Error Message:
538
            # 'Constant statements are not allowed.'
539
            # which is only partial true.
540
            self.error(node, 'Ellipsis statements are not allowed.')
1✔
541
            return
1✔
542
        return self.node_contents_visit(node)
1✔
543

544
    def visit_Interactive(self, node):
1✔
545
        """Allow single mode without restrictions."""
546
        return self.node_contents_visit(node)
1✔
547

548
    def visit_List(self, node):
1✔
549
        """Allow list literals without restrictions."""
550
        return self.node_contents_visit(node)
1✔
551

552
    def visit_Tuple(self, node):
1✔
553
        """Allow tuple literals without restrictions."""
554
        return self.node_contents_visit(node)
1✔
555

556
    def visit_Set(self, node):
1✔
557
        """Allow set literals without restrictions."""
558
        return self.node_contents_visit(node)
1✔
559

560
    def visit_Dict(self, node):
1✔
561
        """Allow dict literals without restrictions."""
562
        return self.node_contents_visit(node)
1✔
563

564
    def visit_FormattedValue(self, node):
1✔
565
        """Allow f-strings without restrictions."""
566
        return self.node_contents_visit(node)
1✔
567

568
    def visit_TemplateStr(self, node):
1✔
569
        """Template strings are allowed by default.
570

571
        As Template strings are a very basic template mechanism, that needs
572
        additional rendering logic to be useful, they are not blocked by
573
        default.
574
        Those rendering logic would be affected by RestrictedPython as well.
575
        """
NEW
576
        return self.node_contents_visit(node)
×
577

578
    def visit_Interpolation(self, node):
1✔
579
        """Interpolations are allowed by default.
580

581
        As Interpolations are part of Template Strings, they are needed
582
        to be reached in the context of RestrictedPython as Template Strings
583
        are allowed. As a user has to provide additional rendering logic
584
        to make use of Template Strings, the security implications of
585
        Interpolations are limited in the context of RestrictedPython.
586
        """
NEW
587
        return self.node_contents_visit(node)
×
588

589
    def visit_JoinedStr(self, node):
1✔
590
        """Allow joined string without restrictions."""
591
        return self.node_contents_visit(node)
1✔
592

593
    # ast for Variables
594

595
    def visit_Name(self, node):
1✔
596
        """Prevents access to protected names.
597

598
        Converts use of the name 'printed' to this expression: '_print()'
599
        """
600

601
        node = self.node_contents_visit(node)
1✔
602

603
        if isinstance(node.ctx, ast.Load):
1✔
604
            if node.id == 'printed':
1✔
605
                self.print_info.printed_used = True
1✔
606
                new_node = ast.Call(
1✔
607
                    func=ast.Name("_print", ast.Load()),
608
                    args=[],
609
                    keywords=[])
610

611
                copy_locations(new_node, node)
1✔
612
                return new_node
1✔
613

614
            elif node.id == 'print':
1✔
615
                self.print_info.print_used = True
1✔
616
                new_node = ast.Attribute(
1✔
617
                    value=ast.Name('_print', ast.Load()),
618
                    attr="_call_print",
619
                    ctx=ast.Load())
620

621
                copy_locations(new_node, node)
1✔
622
                return new_node
1✔
623

624
            self.used_names[node.id] = True
1✔
625

626
        self.check_name(node, node.id)
1✔
627
        return node
1✔
628

629
    def visit_Load(self, node):
1✔
630
        """
631

632
        """
633
        return self.node_contents_visit(node)
1✔
634

635
    def visit_Store(self, node):
1✔
636
        """
637

638
        """
639
        return self.node_contents_visit(node)
1✔
640

641
    def visit_Del(self, node):
1✔
642
        """
643

644
        """
645
        return self.node_contents_visit(node)
1✔
646

647
    def visit_Starred(self, node):
1✔
648
        """
649

650
        """
651
        return self.node_contents_visit(node)
1✔
652

653
    # Expressions
654

655
    def visit_Expression(self, node):
1✔
656
        """Allow Expression statements without restrictions.
657

658
        They are in the AST when using the `eval` compile mode.
659
        """
660
        return self.node_contents_visit(node)
1✔
661

662
    def visit_Expr(self, node):
1✔
663
        """Allow Expr statements (any expression) without restrictions."""
664
        return self.node_contents_visit(node)
1✔
665

666
    def visit_UnaryOp(self, node):
1✔
667
        """
668
        UnaryOp (Unary Operations) is the overall element for:
669
        * Not --> which should be allowed
670
        * UAdd --> Positive notation of variables (e.g. +var)
671
        * USub --> Negative notation of variables (e.g. -var)
672
        """
673
        return self.node_contents_visit(node)
1✔
674

675
    def visit_UAdd(self, node):
1✔
676
        """Allow positive notation of variables. (e.g. +var)"""
677
        return self.node_contents_visit(node)
1✔
678

679
    def visit_USub(self, node):
1✔
680
        """Allow negative notation of variables. (e.g. -var)"""
681
        return self.node_contents_visit(node)
1✔
682

683
    def visit_Not(self, node):
1✔
684
        """Allow the `not` operator."""
685
        return self.node_contents_visit(node)
1✔
686

687
    def visit_Invert(self, node):
1✔
688
        """Allow `~` expressions."""
689
        return self.node_contents_visit(node)
1✔
690

691
    def visit_BinOp(self, node):
1✔
692
        """Allow binary operations."""
693
        return self.node_contents_visit(node)
1✔
694

695
    def visit_Add(self, node):
1✔
696
        """Allow `+` expressions."""
697
        return self.node_contents_visit(node)
1✔
698

699
    def visit_Sub(self, node):
1✔
700
        """Allow `-` expressions."""
701
        return self.node_contents_visit(node)
1✔
702

703
    def visit_Mult(self, node):
1✔
704
        """Allow `*` expressions."""
705
        return self.node_contents_visit(node)
1✔
706

707
    def visit_Div(self, node):
1✔
708
        """Allow `/` expressions."""
709
        return self.node_contents_visit(node)
1✔
710

711
    def visit_FloorDiv(self, node):
1✔
712
        """Allow `//` expressions."""
713
        return self.node_contents_visit(node)
1✔
714

715
    def visit_Mod(self, node):
1✔
716
        """Allow `%` expressions."""
717
        return self.node_contents_visit(node)
1✔
718

719
    def visit_Pow(self, node):
1✔
720
        """Allow `**` expressions."""
721
        return self.node_contents_visit(node)
1✔
722

723
    def visit_LShift(self, node):
1✔
724
        """Allow `<<` expressions."""
725
        return self.node_contents_visit(node)
1✔
726

727
    def visit_RShift(self, node):
1✔
728
        """Allow `>>` expressions."""
729
        return self.node_contents_visit(node)
1✔
730

731
    def visit_BitOr(self, node):
1✔
732
        """Allow `|` expressions."""
733
        return self.node_contents_visit(node)
1✔
734

735
    def visit_BitXor(self, node):
1✔
736
        """Allow `^` expressions."""
737
        return self.node_contents_visit(node)
1✔
738

739
    def visit_BitAnd(self, node):
1✔
740
        """Allow `&` expressions."""
741
        return self.node_contents_visit(node)
1✔
742

743
    def visit_MatMult(self, node):
1✔
744
        """Allow multiplication (`@`)."""
745
        return self.node_contents_visit(node)
1✔
746

747
    def visit_BoolOp(self, node):
1✔
748
        """Allow bool operator without restrictions."""
749
        return self.node_contents_visit(node)
1✔
750

751
    def visit_And(self, node):
1✔
752
        """Allow bool operator `and` without restrictions."""
753
        return self.node_contents_visit(node)
1✔
754

755
    def visit_Or(self, node):
1✔
756
        """Allow bool operator `or` without restrictions."""
757
        return self.node_contents_visit(node)
1✔
758

759
    def visit_Compare(self, node):
1✔
760
        """Allow comparison expressions without restrictions."""
761
        return self.node_contents_visit(node)
1✔
762

763
    def visit_Eq(self, node):
1✔
764
        """Allow == expressions."""
765
        return self.node_contents_visit(node)
1✔
766

767
    def visit_NotEq(self, node):
1✔
768
        """Allow != expressions."""
769
        return self.node_contents_visit(node)
1✔
770

771
    def visit_Lt(self, node):
1✔
772
        """Allow < expressions."""
773
        return self.node_contents_visit(node)
1✔
774

775
    def visit_LtE(self, node):
1✔
776
        """Allow <= expressions."""
777
        return self.node_contents_visit(node)
1✔
778

779
    def visit_Gt(self, node):
1✔
780
        """Allow > expressions."""
781
        return self.node_contents_visit(node)
1✔
782

783
    def visit_GtE(self, node):
1✔
784
        """Allow >= expressions."""
785
        return self.node_contents_visit(node)
1✔
786

787
    def visit_Is(self, node):
1✔
788
        """Allow `is` expressions."""
789
        return self.node_contents_visit(node)
1✔
790

791
    def visit_IsNot(self, node):
1✔
792
        """Allow `is not` expressions."""
793
        return self.node_contents_visit(node)
1✔
794

795
    def visit_In(self, node):
1✔
796
        """Allow `in` expressions."""
797
        return self.node_contents_visit(node)
1✔
798

799
    def visit_NotIn(self, node):
1✔
800
        """Allow `not in` expressions."""
801
        return self.node_contents_visit(node)
1✔
802

803
    def visit_Call(self, node):
1✔
804
        """Checks calls with '*args' and '**kwargs'.
805

806
        Note: The following happens only if '*args' or '**kwargs' is used.
807

808
        Transfroms 'foo(<all the possible ways of args>)' into
809
        _apply_(foo, <all the possible ways for args>)
810

811
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
812
        Python to collapse all the myriad ways to call functions
813
        into one manageable from.
814

815
        From there, '_apply_()' wraps args and kws in guarded accessors,
816
        then calls the function, returning the value.
817
        """
818

819
        if isinstance(node.func, ast.Name):
1✔
820
            if node.func.id == 'exec':
1✔
821
                self.error(node, 'Exec calls are not allowed.')
1✔
822
            elif node.func.id == 'eval':
1✔
823
                self.error(node, 'Eval calls are not allowed.')
1✔
824

825
        needs_wrap = False
1✔
826

827
        for pos_arg in node.args:
1✔
828
            if isinstance(pos_arg, ast.Starred):
1✔
829
                needs_wrap = True
1✔
830

831
        for keyword_arg in node.keywords:
1✔
832
            if keyword_arg.arg is None:
1✔
833
                needs_wrap = True
1✔
834

835
        node = self.node_contents_visit(node)
1✔
836

837
        if not needs_wrap:
1✔
838
            return node
1✔
839

840
        node.args.insert(0, node.func)
1✔
841
        node.func = ast.Name('_apply_', ast.Load())
1✔
842
        copy_locations(node.func, node.args[0])
1✔
843
        return node
1✔
844

845
    def visit_keyword(self, node):
1✔
846
        """
847

848
        """
849
        return self.node_contents_visit(node)
1✔
850

851
    def visit_IfExp(self, node):
1✔
852
        """Allow `if` expressions without restrictions."""
853
        return self.node_contents_visit(node)
1✔
854

855
    def visit_Attribute(self, node):
1✔
856
        """Checks and mutates attribute access/assignment.
857

858
        'a.b' becomes '_getattr_(a, "b")'
859
        'a.b = c' becomes '_write_(a).b = c'
860
        'del a.b' becomes 'del _write_(a).b'
861

862
        The _write_ function should return a security proxy.
863
        """
864
        if node.attr.startswith('_') and node.attr != '_':
1✔
865
            self.error(
1✔
866
                node,
867
                '"{name}" is an invalid attribute name because it starts '
868
                'with "_".'.format(name=node.attr))
869

870
        if node.attr.endswith('__roles__'):
1✔
871
            self.error(
1✔
872
                node,
873
                '"{name}" is an invalid attribute name because it ends '
874
                'with "__roles__".'.format(name=node.attr))
875

876
        if node.attr in INSPECT_ATTRIBUTES:
1✔
877
            self.error(
1✔
878
                node,
879
                f'"{node.attr}" is a restricted name,'
880
                ' that is forbidden to access in RestrictedPython.',
881
            )
882

883
        if isinstance(node.ctx, ast.Load):
1✔
884
            node = self.node_contents_visit(node)
1✔
885
            new_node = ast.Call(
1✔
886
                func=ast.Name('_getattr_', ast.Load()),
887
                args=[node.value, ast.Constant(node.attr)],
888
                keywords=[])
889

890
            copy_locations(new_node, node)
1✔
891
            return new_node
1✔
892

893
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
894
            node = self.node_contents_visit(node)
1✔
895
            new_value = ast.Call(
1✔
896
                func=ast.Name('_write_', ast.Load()),
897
                args=[node.value],
898
                keywords=[])
899

900
            copy_locations(new_value, node.value)
1✔
901
            node.value = new_value
1✔
902
            return node
1✔
903

904
        else:  # pragma: no cover
905
            # Impossible Case only ctx Load, Store and Del are defined in ast.
906
            raise NotImplementedError(
907
                f"Unknown ctx type: {type(node.ctx)}")
908

909
    # Subscripting
910

911
    def visit_Subscript(self, node):
1✔
912
        """Transforms all kinds of subscripts.
913

914
        'foo[bar]' becomes '_getitem_(foo, bar)'
915
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
916
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
917
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
918
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
919
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
920
        'foo[a] = c' becomes '_write_(foo)[a] = c'
921
        'del foo[a]' becomes 'del _write_(foo)[a]'
922

923
        The _write_ function should return a security proxy.
924
        """
925
        node = self.node_contents_visit(node)
1✔
926

927
        # 'AugStore' and 'AugLoad' are defined in 'Python.asdl' as possible
928
        # 'expr_context'. However, according to Python/ast.c
929
        # they are NOT used by the implementation => No need to worry here.
930
        # Instead ast.c creates 'AugAssign' nodes, which can be visited.
931

932
        if isinstance(node.ctx, ast.Load):
1✔
933
            new_node = ast.Call(
1✔
934
                func=ast.Name('_getitem_', ast.Load()),
935
                args=[node.value, self.transform_slice(node.slice)],
936
                keywords=[])
937

938
            copy_locations(new_node, node)
1✔
939
            return new_node
1✔
940

941
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
942
            new_value = ast.Call(
1✔
943
                func=ast.Name('_write_', ast.Load()),
944
                args=[node.value],
945
                keywords=[])
946

947
            copy_locations(new_value, node)
1✔
948
            node.value = new_value
1✔
949
            return node
1✔
950

951
        else:  # pragma: no cover
952
            # Impossible Case only ctx Load, Store and Del are defined in ast.
953
            raise NotImplementedError(
954
                f"Unknown ctx type: {type(node.ctx)}")
955

956
    def visit_Index(self, node):
1✔
957
        """
958

959
        """
960
        return self.node_contents_visit(node)
×
961

962
    def visit_Slice(self, node):
1✔
963
        """
964

965
        """
966
        return self.node_contents_visit(node)
1✔
967

968
    def visit_ExtSlice(self, node):
1✔
969
        """
970

971
        """
972
        return self.node_contents_visit(node)
×
973

974
    # Comprehensions
975

976
    def visit_ListComp(self, node):
1✔
977
        """
978

979
        """
980
        return self.node_contents_visit(node)
1✔
981

982
    def visit_SetComp(self, node):
1✔
983
        """
984

985
        """
986
        return self.node_contents_visit(node)
1✔
987

988
    def visit_GeneratorExp(self, node):
1✔
989
        """
990

991
        """
992
        return self.node_contents_visit(node)
1✔
993

994
    def visit_DictComp(self, node):
1✔
995
        """
996

997
        """
998
        return self.node_contents_visit(node)
1✔
999

1000
    def visit_comprehension(self, node):
1✔
1001
        """
1002

1003
        """
1004
        return self.guard_iter(node)
1✔
1005

1006
    # Statements
1007

1008
    def visit_Assign(self, node):
1✔
1009
        """
1010

1011
        """
1012

1013
        node = self.node_contents_visit(node)
1✔
1014

1015
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
1016
            return node
1✔
1017

1018
        # Handle sequence unpacking.
1019
        # For briefness this example omits cleanup of the temporary variables.
1020
        # Check 'transform_tuple_assign' how its done.
1021
        #
1022
        # - Single target (with nested support)
1023
        # (a, (b, (c, d))) = <exp>
1024
        # is converted to
1025
        # (a, t1) = _getiter_(<exp>)
1026
        # (b, t2) = _getiter_(t1)
1027
        # (c, d) = _getiter_(t2)
1028
        #
1029
        # - Multi targets
1030
        # (a, b) = (c, d) = <exp>
1031
        # is converted to
1032
        # (c, d) = _getiter_(<exp>)
1033
        # (a, b) = _getiter_(<exp>)
1034
        # Why is this valid ? The original bytecode for this multi targets
1035
        # behaves the same way.
1036

1037
        # ast.NodeTransformer works with list results.
1038
        # He injects it at the right place of the node's parent statements.
1039
        new_nodes = []
1✔
1040

1041
        # python fills the right most target first.
1042
        for target in reversed(node.targets):
1✔
1043
            if isinstance(target, ast.Tuple):
1✔
1044
                wrapper = ast.Assign(
1✔
1045
                    targets=[target],
1046
                    value=self.protect_unpack_sequence(target, node.value))
1047
                new_nodes.append(wrapper)
1✔
1048
            else:
1049
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1050
                new_nodes.append(new_node)
1✔
1051

1052
        for new_node in new_nodes:
1✔
1053
            copy_locations(new_node, node)
1✔
1054

1055
        return new_nodes
1✔
1056

1057
    def visit_AugAssign(self, node):
1✔
1058
        """Forbid certain kinds of AugAssign
1059

1060
        According to the language reference (and ast.c) the following nodes
1061
        are are possible:
1062
        Name, Attribute, Subscript
1063

1064
        Note that although augmented assignment of attributes and
1065
        subscripts is disallowed, augmented assignment of names (such
1066
        as 'n += 1') is allowed.
1067
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1068
        """
1069

1070
        node = self.node_contents_visit(node)
1✔
1071

1072
        if isinstance(node.target, ast.Attribute):
1✔
1073
            self.error(
1✔
1074
                node,
1075
                "Augmented assignment of attributes is not allowed.")
1076
            return node
1✔
1077

1078
        elif isinstance(node.target, ast.Subscript):
1✔
1079
            self.error(
1✔
1080
                node,
1081
                "Augmented assignment of object items "
1082
                "and slices is not allowed.")
1083
            return node
1✔
1084

1085
        elif isinstance(node.target, ast.Name):
1✔
1086
            new_node = ast.Assign(
1✔
1087
                targets=[node.target],
1088
                value=ast.Call(
1089
                    func=ast.Name('_inplacevar_', ast.Load()),
1090
                    args=[
1091
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1092
                        ast.Name(node.target.id, ast.Load()),
1093
                        node.value
1094
                    ],
1095
                    keywords=[]))
1096

1097
            copy_locations(new_node, node)
1✔
1098
            return new_node
1✔
1099
        else:  # pragma: no cover
1100
            # Impossible Case - Only Node Types:
1101
            # * Name
1102
            # * Attribute
1103
            # * Subscript
1104
            # defined, those are checked before.
1105
            raise NotImplementedError(
1106
                f"Unknown target type: {type(node.target)}")
1107

1108
    def visit_Raise(self, node):
1✔
1109
        """Allow `raise` statements without restrictions."""
1110
        return self.node_contents_visit(node)
1✔
1111

1112
    def visit_Assert(self, node):
1✔
1113
        """Allow assert statements without restrictions."""
1114
        return self.node_contents_visit(node)
1✔
1115

1116
    def visit_Delete(self, node):
1✔
1117
        """Allow `del` statements without restrictions."""
1118
        return self.node_contents_visit(node)
1✔
1119

1120
    def visit_Pass(self, node):
1✔
1121
        """Allow `pass` statements without restrictions."""
1122
        return self.node_contents_visit(node)
1✔
1123

1124
    # Imports
1125

1126
    def visit_Import(self, node):
1✔
1127
        """Allow `import` statements with restrictions.
1128
        See check_import_names."""
1129
        return self.check_import_names(node)
1✔
1130

1131
    def visit_ImportFrom(self, node):
1✔
1132
        """Allow `import from` statements with restrictions.
1133
        See check_import_names."""
1134
        return self.check_import_names(node)
1✔
1135

1136
    def visit_alias(self, node):
1✔
1137
        """Allow `as` statements in import and import from statements."""
1138
        return self.node_contents_visit(node)
1✔
1139

1140
    # Control flow
1141

1142
    def visit_If(self, node):
1✔
1143
        """Allow `if` statements without restrictions."""
1144
        return self.node_contents_visit(node)
1✔
1145

1146
    def visit_For(self, node):
1✔
1147
        """Allow `for` statements with some restrictions."""
1148
        return self.guard_iter(node)
1✔
1149

1150
    def visit_While(self, node):
1✔
1151
        """Allow `while` statements."""
1152
        return self.node_contents_visit(node)
1✔
1153

1154
    def visit_Break(self, node):
1✔
1155
        """Allow `break` statements without restrictions."""
1156
        return self.node_contents_visit(node)
1✔
1157

1158
    def visit_Continue(self, node):
1✔
1159
        """Allow `continue` statements without restrictions."""
1160
        return self.node_contents_visit(node)
1✔
1161

1162
    def visit_Try(self, node):
1✔
1163
        """Allow `try` without restrictions."""
1164
        return self.node_contents_visit(node)
1✔
1165

1166
    def visit_TryStar(self, node):
1✔
1167
        """Disallow `ExceptionGroup` due to a potential sandbox escape."""
1168
        self.not_allowed(node)
1✔
1169

1170
    def visit_ExceptHandler(self, node):
1✔
1171
        """Protect exception handlers."""
1172
        node = self.node_contents_visit(node)
1✔
1173
        self.check_name(node, node.name)
1✔
1174
        return node
1✔
1175

1176
    def visit_With(self, node):
1✔
1177
        """Protect tuple unpacking on with statements."""
1178
        node = self.node_contents_visit(node)
1✔
1179

1180
        for item in reversed(node.items):
1✔
1181
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1182
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1183
                    node,
1184
                    item.optional_vars)
1185

1186
                item.optional_vars = tmp_target
1✔
1187
                node.body.insert(0, unpack)
1✔
1188

1189
        return node
1✔
1190

1191
    def visit_withitem(self, node):
1✔
1192
        """Allow `with` statements (context managers) without restrictions."""
1193
        return self.node_contents_visit(node)
1✔
1194

1195
    # Function and class definitions
1196

1197
    def visit_FunctionDef(self, node):
1✔
1198
        """Allow function definitions (`def`) with some restrictions."""
1199
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1200
        self.check_function_argument_names(node)
1✔
1201

1202
        with self.print_info.new_print_scope():
1✔
1203
            node = self.node_contents_visit(node)
1✔
1204
            self.inject_print_collector(node)
1✔
1205
        return node
1✔
1206

1207
    def visit_Lambda(self, node):
1✔
1208
        """Allow lambda with some restrictions."""
1209
        self.check_function_argument_names(node)
1✔
1210
        return self.node_contents_visit(node)
1✔
1211

1212
    def visit_arguments(self, node):
1✔
1213
        """
1214

1215
        """
1216
        return self.node_contents_visit(node)
1✔
1217

1218
    def visit_arg(self, node):
1✔
1219
        """
1220

1221
        """
1222
        return self.node_contents_visit(node)
1✔
1223

1224
    def visit_Return(self, node):
1✔
1225
        """Allow `return` statements without restrictions."""
1226
        return self.node_contents_visit(node)
1✔
1227

1228
    def visit_Yield(self, node):
1✔
1229
        """Allow `yield`statements without restrictions."""
1230
        return self.node_contents_visit(node)
1✔
1231

1232
    def visit_YieldFrom(self, node):
1✔
1233
        """Allow `yield`statements without restrictions."""
1234
        return self.node_contents_visit(node)
1✔
1235

1236
    def visit_Global(self, node):
1✔
1237
        """Allow `global` statements without restrictions."""
1238
        return self.node_contents_visit(node)
1✔
1239

1240
    def visit_Nonlocal(self, node):
1✔
1241
        """Deny `nonlocal` statements."""
1242
        self.not_allowed(node)
1✔
1243

1244
    def visit_ClassDef(self, node):
1✔
1245
        """Check the name of a class definition."""
1246
        self.check_name(node, node.name)
1✔
1247
        node = self.node_contents_visit(node)
1✔
1248
        if any(keyword.arg == 'metaclass' for keyword in node.keywords):
1✔
1249
            self.error(
1✔
1250
                node, 'The keyword argument "metaclass" is not allowed.')
1251
        CLASS_DEF = textwrap.dedent('''\
1✔
1252
            class {0.name}(metaclass=__metaclass__):
1253
                pass
1254
        '''.format(node))
1255
        new_class_node = ast.parse(CLASS_DEF).body[0]
1✔
1256
        new_class_node.body = node.body
1✔
1257
        new_class_node.bases = node.bases
1✔
1258
        new_class_node.decorator_list = node.decorator_list
1✔
1259
        return new_class_node
1✔
1260

1261
    def visit_Module(self, node):
1✔
1262
        """Add the print_collector (only if print is used) at the top."""
1263
        node = self.node_contents_visit(node)
1✔
1264

1265
        # Inject the print collector after 'from __future__ import ....'
1266
        position = 0
1✔
1267
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1268
            if not isinstance(child, ast.ImportFrom):
1✔
1269
                break
1✔
1270

1271
            if not child.module == '__future__':
1✔
1272
                break
1✔
1273

1274
        self.inject_print_collector(node, position)
1✔
1275
        return node
1✔
1276

1277
    # Async und await
1278

1279
    def visit_AsyncFunctionDef(self, node):
1✔
1280
        """Deny async functions."""
1281
        self.not_allowed(node)
1✔
1282

1283
    def visit_Await(self, node):
1✔
1284
        """Deny async functionality."""
1285
        self.not_allowed(node)
1✔
1286

1287
    def visit_AsyncFor(self, node):
1✔
1288
        """Deny async functionality."""
1289
        self.not_allowed(node)
1✔
1290

1291
    def visit_AsyncWith(self, node):
1✔
1292
        """Deny async functionality."""
1293
        self.not_allowed(node)
1✔
1294

1295
    # Assignment expressions (walrus operator ``:=``)
1296
    # New in 3.8
1297
    def visit_NamedExpr(self, node):
1✔
1298
        """Allow assignment expressions under some circumstances."""
1299
        # while the grammar requires ``node.target`` to be a ``Name``
1300
        # the abstract syntax is more permissive and allows an ``expr``.
1301
        # We support only a ``Name``.
1302
        # This is safe as the expression can only add/modify local
1303
        # variables. While this may hide global variables, an
1304
        # (implicitly performed) name check guarantees (as usual)
1305
        # that no essential global variable is hidden.
1306
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1307
        target = node.target
1✔
1308
        if not isinstance(target, ast.Name):
1✔
1309
            self.error(
1✔
1310
                node,
1311
                "Assignment expressions are only allowed for simple targets")
1312
        return node
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc