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

zopefoundation / RestrictedPython / 18631406064

19 Oct 2025 01:55PM UTC coverage: 97.895% (-0.9%) from 98.772%
18631406064

Pull #304

github

dataflake
- use a pinned commit hash for astral/setup-uv action
Pull Request #304: Add Python 3.14 support

214 of 233 branches covered (91.85%)

18 of 41 new or added lines in 3 files covered. (43.9%)

3 existing lines in 1 file now uncovered.

2511 of 2565 relevant lines covered (97.89%)

0.98 hits per line

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

94.79
/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
        self.warn(
×
577
            node,
578
            'TemplateStr statements are not yet allowed, '
579
            'please use f-strings or a real template engine instead.')
580
        # self.not_allowed(node)
NEW
581
        return self.node_contents_visit(node)
×
582

583
    def visit_Interpolation(self, node):
1✔
584
        """Interpolations are allowed by default.
585

586
        As Interpolations are part of Template Strings, they are needed
587
        to be reached in the context of RestrictedPython as Template Strings
588
        are allowed. As a user has to provide additional rendering logic
589
        to make use of Template Strings, the security implications of
590
        Interpolations are limited in the context of RestrictedPython.
591
        """
592
        # self.not_allowed(node)
NEW
UNCOV
593
        return self.node_contents_visit(node)
×
594

595
    def visit_JoinedStr(self, node):
1✔
596
        """Allow joined string without restrictions."""
597
        return self.node_contents_visit(node)
1✔
598

599
    # ast for Variables
600

601
    def visit_Name(self, node):
1✔
602
        """Prevents access to protected names.
603

604
        Converts use of the name 'printed' to this expression: '_print()'
605
        """
606

607
        node = self.node_contents_visit(node)
1✔
608

609
        if isinstance(node.ctx, ast.Load):
1✔
610
            if node.id == 'printed':
1✔
611
                self.print_info.printed_used = True
1✔
612
                new_node = ast.Call(
1✔
613
                    func=ast.Name("_print", ast.Load()),
614
                    args=[],
615
                    keywords=[])
616

617
                copy_locations(new_node, node)
1✔
618
                return new_node
1✔
619

620
            elif node.id == 'print':
1✔
621
                self.print_info.print_used = True
1✔
622
                new_node = ast.Attribute(
1✔
623
                    value=ast.Name('_print', ast.Load()),
624
                    attr="_call_print",
625
                    ctx=ast.Load())
626

627
                copy_locations(new_node, node)
1✔
628
                return new_node
1✔
629

630
            self.used_names[node.id] = True
1✔
631

632
        self.check_name(node, node.id)
1✔
633
        return node
1✔
634

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

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

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

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

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

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

653
    def visit_Starred(self, node):
1✔
654
        """
655

656
        """
657
        return self.node_contents_visit(node)
1✔
658

659
    # Expressions
660

661
    def visit_Expression(self, node):
1✔
662
        """Allow Expression statements without restrictions.
663

664
        They are in the AST when using the `eval` compile mode.
665
        """
666
        return self.node_contents_visit(node)
1✔
667

668
    def visit_Expr(self, node):
1✔
669
        """Allow Expr statements (any expression) without restrictions."""
670
        return self.node_contents_visit(node)
1✔
671

672
    def visit_UnaryOp(self, node):
1✔
673
        """
674
        UnaryOp (Unary Operations) is the overall element for:
675
        * Not --> which should be allowed
676
        * UAdd --> Positive notation of variables (e.g. +var)
677
        * USub --> Negative notation of variables (e.g. -var)
678
        """
679
        return self.node_contents_visit(node)
1✔
680

681
    def visit_UAdd(self, node):
1✔
682
        """Allow positive notation of variables. (e.g. +var)"""
683
        return self.node_contents_visit(node)
1✔
684

685
    def visit_USub(self, node):
1✔
686
        """Allow negative notation of variables. (e.g. -var)"""
687
        return self.node_contents_visit(node)
1✔
688

689
    def visit_Not(self, node):
1✔
690
        """Allow the `not` operator."""
691
        return self.node_contents_visit(node)
1✔
692

693
    def visit_Invert(self, node):
1✔
694
        """Allow `~` expressions."""
695
        return self.node_contents_visit(node)
1✔
696

697
    def visit_BinOp(self, node):
1✔
698
        """Allow binary operations."""
699
        return self.node_contents_visit(node)
1✔
700

701
    def visit_Add(self, node):
1✔
702
        """Allow `+` expressions."""
703
        return self.node_contents_visit(node)
1✔
704

705
    def visit_Sub(self, node):
1✔
706
        """Allow `-` expressions."""
707
        return self.node_contents_visit(node)
1✔
708

709
    def visit_Mult(self, node):
1✔
710
        """Allow `*` expressions."""
711
        return self.node_contents_visit(node)
1✔
712

713
    def visit_Div(self, node):
1✔
714
        """Allow `/` expressions."""
715
        return self.node_contents_visit(node)
1✔
716

717
    def visit_FloorDiv(self, node):
1✔
718
        """Allow `//` expressions."""
719
        return self.node_contents_visit(node)
1✔
720

721
    def visit_Mod(self, node):
1✔
722
        """Allow `%` expressions."""
723
        return self.node_contents_visit(node)
1✔
724

725
    def visit_Pow(self, node):
1✔
726
        """Allow `**` expressions."""
727
        return self.node_contents_visit(node)
1✔
728

729
    def visit_LShift(self, node):
1✔
730
        """Allow `<<` expressions."""
731
        return self.node_contents_visit(node)
1✔
732

733
    def visit_RShift(self, node):
1✔
734
        """Allow `>>` expressions."""
735
        return self.node_contents_visit(node)
1✔
736

737
    def visit_BitOr(self, node):
1✔
738
        """Allow `|` expressions."""
739
        return self.node_contents_visit(node)
1✔
740

741
    def visit_BitXor(self, node):
1✔
742
        """Allow `^` expressions."""
743
        return self.node_contents_visit(node)
1✔
744

745
    def visit_BitAnd(self, node):
1✔
746
        """Allow `&` expressions."""
747
        return self.node_contents_visit(node)
1✔
748

749
    def visit_MatMult(self, node):
1✔
750
        """Allow multiplication (`@`)."""
751
        return self.node_contents_visit(node)
1✔
752

753
    def visit_BoolOp(self, node):
1✔
754
        """Allow bool operator without restrictions."""
755
        return self.node_contents_visit(node)
1✔
756

757
    def visit_And(self, node):
1✔
758
        """Allow bool operator `and` without restrictions."""
759
        return self.node_contents_visit(node)
1✔
760

761
    def visit_Or(self, node):
1✔
762
        """Allow bool operator `or` without restrictions."""
763
        return self.node_contents_visit(node)
1✔
764

765
    def visit_Compare(self, node):
1✔
766
        """Allow comparison expressions without restrictions."""
767
        return self.node_contents_visit(node)
1✔
768

769
    def visit_Eq(self, node):
1✔
770
        """Allow == expressions."""
771
        return self.node_contents_visit(node)
1✔
772

773
    def visit_NotEq(self, node):
1✔
774
        """Allow != expressions."""
775
        return self.node_contents_visit(node)
1✔
776

777
    def visit_Lt(self, node):
1✔
778
        """Allow < expressions."""
779
        return self.node_contents_visit(node)
1✔
780

781
    def visit_LtE(self, node):
1✔
782
        """Allow <= expressions."""
783
        return self.node_contents_visit(node)
1✔
784

785
    def visit_Gt(self, node):
1✔
786
        """Allow > expressions."""
787
        return self.node_contents_visit(node)
1✔
788

789
    def visit_GtE(self, node):
1✔
790
        """Allow >= expressions."""
791
        return self.node_contents_visit(node)
1✔
792

793
    def visit_Is(self, node):
1✔
794
        """Allow `is` expressions."""
795
        return self.node_contents_visit(node)
1✔
796

797
    def visit_IsNot(self, node):
1✔
798
        """Allow `is not` expressions."""
799
        return self.node_contents_visit(node)
1✔
800

801
    def visit_In(self, node):
1✔
802
        """Allow `in` expressions."""
803
        return self.node_contents_visit(node)
1✔
804

805
    def visit_NotIn(self, node):
1✔
806
        """Allow `not in` expressions."""
807
        return self.node_contents_visit(node)
1✔
808

809
    def visit_Call(self, node):
1✔
810
        """Checks calls with '*args' and '**kwargs'.
811

812
        Note: The following happens only if '*args' or '**kwargs' is used.
813

814
        Transfroms 'foo(<all the possible ways of args>)' into
815
        _apply_(foo, <all the possible ways for args>)
816

817
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
818
        Python to collapse all the myriad ways to call functions
819
        into one manageable from.
820

821
        From there, '_apply_()' wraps args and kws in guarded accessors,
822
        then calls the function, returning the value.
823
        """
824

825
        if isinstance(node.func, ast.Name):
1✔
826
            if node.func.id == 'exec':
1✔
827
                self.error(node, 'Exec calls are not allowed.')
1✔
828
            elif node.func.id == 'eval':
1✔
829
                self.error(node, 'Eval calls are not allowed.')
1✔
830

831
        needs_wrap = False
1✔
832

833
        for pos_arg in node.args:
1✔
834
            if isinstance(pos_arg, ast.Starred):
1✔
835
                needs_wrap = True
1✔
836

837
        for keyword_arg in node.keywords:
1✔
838
            if keyword_arg.arg is None:
1✔
839
                needs_wrap = True
1✔
840

841
        node = self.node_contents_visit(node)
1✔
842

843
        if not needs_wrap:
1✔
844
            return node
1✔
845

846
        node.args.insert(0, node.func)
1✔
847
        node.func = ast.Name('_apply_', ast.Load())
1✔
848
        copy_locations(node.func, node.args[0])
1✔
849
        return node
1✔
850

851
    def visit_keyword(self, node):
1✔
852
        """
853

854
        """
855
        return self.node_contents_visit(node)
1✔
856

857
    def visit_IfExp(self, node):
1✔
858
        """Allow `if` expressions without restrictions."""
859
        return self.node_contents_visit(node)
1✔
860

861
    def visit_Attribute(self, node):
1✔
862
        """Checks and mutates attribute access/assignment.
863

864
        'a.b' becomes '_getattr_(a, "b")'
865
        'a.b = c' becomes '_write_(a).b = c'
866
        'del a.b' becomes 'del _write_(a).b'
867

868
        The _write_ function should return a security proxy.
869
        """
870
        if node.attr.startswith('_') and node.attr != '_':
1✔
871
            self.error(
1✔
872
                node,
873
                '"{name}" is an invalid attribute name because it starts '
874
                'with "_".'.format(name=node.attr))
875

876
        if node.attr.endswith('__roles__'):
1✔
877
            self.error(
1✔
878
                node,
879
                '"{name}" is an invalid attribute name because it ends '
880
                'with "__roles__".'.format(name=node.attr))
881

882
        if node.attr in INSPECT_ATTRIBUTES:
1✔
883
            self.error(
1✔
884
                node,
885
                f'"{node.attr}" is a restricted name,'
886
                ' that is forbidden to access in RestrictedPython.',
887
            )
888

889
        if isinstance(node.ctx, ast.Load):
1✔
890
            node = self.node_contents_visit(node)
1✔
891
            new_node = ast.Call(
1✔
892
                func=ast.Name('_getattr_', ast.Load()),
893
                args=[node.value, ast.Constant(node.attr)],
894
                keywords=[])
895

896
            copy_locations(new_node, node)
1✔
897
            return new_node
1✔
898

899
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
900
            node = self.node_contents_visit(node)
1✔
901
            new_value = ast.Call(
1✔
902
                func=ast.Name('_write_', ast.Load()),
903
                args=[node.value],
904
                keywords=[])
905

906
            copy_locations(new_value, node.value)
1✔
907
            node.value = new_value
1✔
908
            return node
1✔
909

910
        else:  # pragma: no cover
911
            # Impossible Case only ctx Load, Store and Del are defined in ast.
912
            raise NotImplementedError(
913
                f"Unknown ctx type: {type(node.ctx)}")
914

915
    # Subscripting
916

917
    def visit_Subscript(self, node):
1✔
918
        """Transforms all kinds of subscripts.
919

920
        'foo[bar]' becomes '_getitem_(foo, bar)'
921
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
922
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
923
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
924
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
925
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
926
        'foo[a] = c' becomes '_write_(foo)[a] = c'
927
        'del foo[a]' becomes 'del _write_(foo)[a]'
928

929
        The _write_ function should return a security proxy.
930
        """
931
        node = self.node_contents_visit(node)
1✔
932

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

938
        if isinstance(node.ctx, ast.Load):
1✔
939
            new_node = ast.Call(
1✔
940
                func=ast.Name('_getitem_', ast.Load()),
941
                args=[node.value, self.transform_slice(node.slice)],
942
                keywords=[])
943

944
            copy_locations(new_node, node)
1✔
945
            return new_node
1✔
946

947
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
948
            new_value = ast.Call(
1✔
949
                func=ast.Name('_write_', ast.Load()),
950
                args=[node.value],
951
                keywords=[])
952

953
            copy_locations(new_value, node)
1✔
954
            node.value = new_value
1✔
955
            return node
1✔
956

957
        else:  # pragma: no cover
958
            # Impossible Case only ctx Load, Store and Del are defined in ast.
959
            raise NotImplementedError(
960
                f"Unknown ctx type: {type(node.ctx)}")
961

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

965
        """
UNCOV
966
        return self.node_contents_visit(node)
×
967

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

971
        """
972
        return self.node_contents_visit(node)
1✔
973

974
    def visit_ExtSlice(self, node):
1✔
975
        """
976

977
        """
UNCOV
978
        return self.node_contents_visit(node)
×
979

980
    # Comprehensions
981

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

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

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

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

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

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

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

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

1006
    def visit_comprehension(self, node):
1✔
1007
        """
1008

1009
        """
1010
        return self.guard_iter(node)
1✔
1011

1012
    # Statements
1013

1014
    def visit_Assign(self, node):
1✔
1015
        """
1016

1017
        """
1018

1019
        node = self.node_contents_visit(node)
1✔
1020

1021
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
1022
            return node
1✔
1023

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

1043
        # ast.NodeTransformer works with list results.
1044
        # He injects it at the right place of the node's parent statements.
1045
        new_nodes = []
1✔
1046

1047
        # python fills the right most target first.
1048
        for target in reversed(node.targets):
1✔
1049
            if isinstance(target, ast.Tuple):
1✔
1050
                wrapper = ast.Assign(
1✔
1051
                    targets=[target],
1052
                    value=self.protect_unpack_sequence(target, node.value))
1053
                new_nodes.append(wrapper)
1✔
1054
            else:
1055
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1056
                new_nodes.append(new_node)
1✔
1057

1058
        for new_node in new_nodes:
1✔
1059
            copy_locations(new_node, node)
1✔
1060

1061
        return new_nodes
1✔
1062

1063
    def visit_AugAssign(self, node):
1✔
1064
        """Forbid certain kinds of AugAssign
1065

1066
        According to the language reference (and ast.c) the following nodes
1067
        are are possible:
1068
        Name, Attribute, Subscript
1069

1070
        Note that although augmented assignment of attributes and
1071
        subscripts is disallowed, augmented assignment of names (such
1072
        as 'n += 1') is allowed.
1073
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1074
        """
1075

1076
        node = self.node_contents_visit(node)
1✔
1077

1078
        if isinstance(node.target, ast.Attribute):
1✔
1079
            self.error(
1✔
1080
                node,
1081
                "Augmented assignment of attributes is not allowed.")
1082
            return node
1✔
1083

1084
        elif isinstance(node.target, ast.Subscript):
1✔
1085
            self.error(
1✔
1086
                node,
1087
                "Augmented assignment of object items "
1088
                "and slices is not allowed.")
1089
            return node
1✔
1090

1091
        elif isinstance(node.target, ast.Name):
1✔
1092
            new_node = ast.Assign(
1✔
1093
                targets=[node.target],
1094
                value=ast.Call(
1095
                    func=ast.Name('_inplacevar_', ast.Load()),
1096
                    args=[
1097
                        ast.Constant(IOPERATOR_TO_STR[type(node.op)]),
1098
                        ast.Name(node.target.id, ast.Load()),
1099
                        node.value
1100
                    ],
1101
                    keywords=[]))
1102

1103
            copy_locations(new_node, node)
1✔
1104
            return new_node
1✔
1105
        else:  # pragma: no cover
1106
            # Impossible Case - Only Node Types:
1107
            # * Name
1108
            # * Attribute
1109
            # * Subscript
1110
            # defined, those are checked before.
1111
            raise NotImplementedError(
1112
                f"Unknown target type: {type(node.target)}")
1113

1114
    def visit_Raise(self, node):
1✔
1115
        """Allow `raise` statements without restrictions."""
1116
        return self.node_contents_visit(node)
1✔
1117

1118
    def visit_Assert(self, node):
1✔
1119
        """Allow assert statements without restrictions."""
1120
        return self.node_contents_visit(node)
1✔
1121

1122
    def visit_Delete(self, node):
1✔
1123
        """Allow `del` statements without restrictions."""
1124
        return self.node_contents_visit(node)
1✔
1125

1126
    def visit_Pass(self, node):
1✔
1127
        """Allow `pass` statements without restrictions."""
1128
        return self.node_contents_visit(node)
1✔
1129

1130
    # Imports
1131

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

1137
    def visit_ImportFrom(self, node):
1✔
1138
        """Allow `import from` statements with restrictions.
1139
        See check_import_names."""
1140
        return self.check_import_names(node)
1✔
1141

1142
    def visit_alias(self, node):
1✔
1143
        """Allow `as` statements in import and import from statements."""
1144
        return self.node_contents_visit(node)
1✔
1145

1146
    # Control flow
1147

1148
    def visit_If(self, node):
1✔
1149
        """Allow `if` statements without restrictions."""
1150
        return self.node_contents_visit(node)
1✔
1151

1152
    def visit_For(self, node):
1✔
1153
        """Allow `for` statements with some restrictions."""
1154
        return self.guard_iter(node)
1✔
1155

1156
    def visit_While(self, node):
1✔
1157
        """Allow `while` statements."""
1158
        return self.node_contents_visit(node)
1✔
1159

1160
    def visit_Break(self, node):
1✔
1161
        """Allow `break` statements without restrictions."""
1162
        return self.node_contents_visit(node)
1✔
1163

1164
    def visit_Continue(self, node):
1✔
1165
        """Allow `continue` statements without restrictions."""
1166
        return self.node_contents_visit(node)
1✔
1167

1168
    def visit_Try(self, node):
1✔
1169
        """Allow `try` without restrictions."""
1170
        return self.node_contents_visit(node)
1✔
1171

1172
    def visit_TryStar(self, node):
1✔
1173
        """Disallow `ExceptionGroup` due to a potential sandbox escape."""
1174
        self.not_allowed(node)
1✔
1175

1176
    def visit_ExceptHandler(self, node):
1✔
1177
        """Protect exception handlers."""
1178
        node = self.node_contents_visit(node)
1✔
1179
        self.check_name(node, node.name)
1✔
1180
        return node
1✔
1181

1182
    def visit_With(self, node):
1✔
1183
        """Protect tuple unpacking on with statements."""
1184
        node = self.node_contents_visit(node)
1✔
1185

1186
        for item in reversed(node.items):
1✔
1187
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1188
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1189
                    node,
1190
                    item.optional_vars)
1191

1192
                item.optional_vars = tmp_target
1✔
1193
                node.body.insert(0, unpack)
1✔
1194

1195
        return node
1✔
1196

1197
    def visit_withitem(self, node):
1✔
1198
        """Allow `with` statements (context managers) without restrictions."""
1199
        return self.node_contents_visit(node)
1✔
1200

1201
    # Function and class definitions
1202

1203
    def visit_FunctionDef(self, node):
1✔
1204
        """Allow function definitions (`def`) with some restrictions."""
1205
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1206
        self.check_function_argument_names(node)
1✔
1207

1208
        with self.print_info.new_print_scope():
1✔
1209
            node = self.node_contents_visit(node)
1✔
1210
            self.inject_print_collector(node)
1✔
1211
        return node
1✔
1212

1213
    def visit_Lambda(self, node):
1✔
1214
        """Allow lambda with some restrictions."""
1215
        self.check_function_argument_names(node)
1✔
1216
        return self.node_contents_visit(node)
1✔
1217

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

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

1224
    def visit_arg(self, node):
1✔
1225
        """
1226

1227
        """
1228
        return self.node_contents_visit(node)
1✔
1229

1230
    def visit_Return(self, node):
1✔
1231
        """Allow `return` statements without restrictions."""
1232
        return self.node_contents_visit(node)
1✔
1233

1234
    def visit_Yield(self, node):
1✔
1235
        """Allow `yield`statements without restrictions."""
1236
        return self.node_contents_visit(node)
1✔
1237

1238
    def visit_YieldFrom(self, node):
1✔
1239
        """Allow `yield`statements without restrictions."""
1240
        return self.node_contents_visit(node)
1✔
1241

1242
    def visit_Global(self, node):
1✔
1243
        """Allow `global` statements without restrictions."""
1244
        return self.node_contents_visit(node)
1✔
1245

1246
    def visit_Nonlocal(self, node):
1✔
1247
        """Deny `nonlocal` statements."""
1248
        self.not_allowed(node)
1✔
1249

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

1267
    def visit_Module(self, node):
1✔
1268
        """Add the print_collector (only if print is used) at the top."""
1269
        node = self.node_contents_visit(node)
1✔
1270

1271
        # Inject the print collector after 'from __future__ import ....'
1272
        position = 0
1✔
1273
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1274
            if not isinstance(child, ast.ImportFrom):
1✔
1275
                break
1✔
1276

1277
            if not child.module == '__future__':
1✔
1278
                break
1✔
1279

1280
        self.inject_print_collector(node, position)
1✔
1281
        return node
1✔
1282

1283
    # Async und await
1284

1285
    def visit_AsyncFunctionDef(self, node):
1✔
1286
        """Deny async functions."""
1287
        self.not_allowed(node)
1✔
1288

1289
    def visit_Await(self, node):
1✔
1290
        """Deny async functionality."""
1291
        self.not_allowed(node)
1✔
1292

1293
    def visit_AsyncFor(self, node):
1✔
1294
        """Deny async functionality."""
1295
        self.not_allowed(node)
1✔
1296

1297
    def visit_AsyncWith(self, node):
1✔
1298
        """Deny async functionality."""
1299
        self.not_allowed(node)
1✔
1300

1301
    # Assignment expressions (walrus operator ``:=``)
1302
    # New in 3.8
1303
    def visit_NamedExpr(self, node):
1✔
1304
        """Allow assignment expressions under some circumstances."""
1305
        # while the grammar requires ``node.target`` to be a ``Name``
1306
        # the abstract syntax is more permissive and allows an ``expr``.
1307
        # We support only a ``Name``.
1308
        # This is safe as the expression can only add/modify local
1309
        # variables. While this may hide global variables, an
1310
        # (implicitly performed) name check guarantees (as usual)
1311
        # that no essential global variable is hidden.
1312
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1313
        target = node.target
1✔
1314
        if not isinstance(target, ast.Name):
1✔
1315
            self.error(
1✔
1316
                node,
1317
                "Assignment expressions are only allowed for simple targets")
1318
        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