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

zopefoundation / RestrictedPython / 3655328316

pending completion
3655328316

push

github

Michael Howitz
Fix GHA: ubuntu-latest no longer contains Python 3.6

337 of 350 branches covered (96.29%)

2439 of 2467 relevant lines covered (98.87%)

0.99 hits per line

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

97.2
/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
"""
1✔
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
from ._compat import IS_PY38_OR_GREATER
1✔
26

27

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

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

58

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

66

67
# When new ast nodes are generated they have no 'lineno', 'end_lineno',
68
# 'col_offset' and 'end_col_offset'. This function copies these fields from the
69
# incoming node:
70
def copy_locations(new_node, old_node):
1✔
71
    assert 'lineno' in new_node._attributes
1✔
72
    new_node.lineno = old_node.lineno
1✔
73

74
    if IS_PY38_OR_GREATER:
1!
75
        assert 'end_lineno' in new_node._attributes
1✔
76
        new_node.end_lineno = old_node.end_lineno
1✔
77

78
    assert 'col_offset' in new_node._attributes
1✔
79
    new_node.col_offset = old_node.col_offset
1✔
80

81
    if IS_PY38_OR_GREATER:
1!
82
        assert 'end_col_offset' in new_node._attributes
1✔
83
        new_node.end_col_offset = old_node.end_col_offset
1✔
84

85
    ast.fix_missing_locations(new_node)
1✔
86

87

88
class PrintInfo:
1✔
89
    def __init__(self):
1✔
90
        self.print_used = False
1✔
91
        self.printed_used = False
1✔
92

93
    @contextlib.contextmanager
1✔
94
    def new_print_scope(self):
1✔
95
        old_print_used = self.print_used
1✔
96
        old_printed_used = self.printed_used
1✔
97

98
        self.print_used = False
1✔
99
        self.printed_used = False
1✔
100

101
        try:
1✔
102
            yield
1✔
103
        finally:
104
            self.print_used = old_print_used
1✔
105
            self.printed_used = old_printed_used
1✔
106

107

108
class RestrictingNodeTransformer(ast.NodeTransformer):
1✔
109

110
    def __init__(self, errors=None, warnings=None, used_names=None):
1✔
111
        super().__init__()
1✔
112
        self.errors = [] if errors is None else errors
1✔
113
        self.warnings = [] if warnings is None else warnings
1✔
114

115
        # All the variables used by the incoming source.
116
        # Internal names/variables, like the ones from 'gen_tmp_name', don't
117
        # have to be added.
118
        # 'used_names' is for example needed by 'RestrictionCapableEval' to
119
        # know wich names it has to supply when calling the final code.
120
        self.used_names = {} if used_names is None else used_names
1✔
121

122
        # Global counter to construct temporary variable names.
123
        self._tmp_idx = 0
1✔
124

125
        self.print_info = PrintInfo()
1✔
126

127
    def gen_tmp_name(self):
1✔
128
        # 'check_name' ensures that no variable is prefixed with '_'.
129
        # => Its safe to use '_tmp..' as a temporary variable.
130
        name = '_tmp%i' % self._tmp_idx
1✔
131
        self._tmp_idx += 1
1✔
132
        return name
1✔
133

134
    def error(self, node, info):
1✔
135
        """Record a security error discovered during transformation."""
136
        lineno = getattr(node, 'lineno', None)
1✔
137
        self.errors.append(
1✔
138
            f'Line {lineno}: {info}')
139

140
    def warn(self, node, info):
1✔
141
        """Record a security error discovered during transformation."""
142
        lineno = getattr(node, 'lineno', None)
1✔
143
        self.warnings.append(
1✔
144
            f'Line {lineno}: {info}')
145

146
    def guard_iter(self, node):
1✔
147
        """
148
        Converts:
149
            for x in expr
150
        to
151
            for x in _getiter_(expr)
152

153
        Also used for
154
        * list comprehensions
155
        * dict comprehensions
156
        * set comprehensions
157
        * generator expresions
158
        """
159
        node = self.node_contents_visit(node)
1✔
160

161
        if isinstance(node.target, ast.Tuple):
1✔
162
            spec = self.gen_unpack_spec(node.target)
1✔
163
            new_iter = ast.Call(
1✔
164
                func=ast.Name('_iter_unpack_sequence_', ast.Load()),
165
                args=[node.iter, spec, ast.Name('_getiter_', ast.Load())],
166
                keywords=[])
167
        else:
168
            new_iter = ast.Call(
1✔
169
                func=ast.Name("_getiter_", ast.Load()),
170
                args=[node.iter],
171
                keywords=[])
172

173
        copy_locations(new_iter, node.iter)
1✔
174
        node.iter = new_iter
1✔
175
        return node
1✔
176

177
    def is_starred(self, ob):
1✔
178
        return isinstance(ob, ast.Starred)
1✔
179

180
    def gen_unpack_spec(self, tpl):
1✔
181
        """Generate a specification for 'guarded_unpack_sequence'.
182

183
        This spec is used to protect sequence unpacking.
184
        The primary goal of this spec is to tell which elements in a sequence
185
        are sequences again. These 'child' sequences have to be protected
186
        again.
187

188
        For example there is a sequence like this:
189
            (a, (b, c), (d, (e, f))) = g
190

191
        On a higher level the spec says:
192
            - There is a sequence of len 3
193
            - The element at index 1 is a sequence again with len 2
194
            - The element at index 2 is a sequence again with len 2
195
              - The element at index 1 in this subsequence is a sequence again
196
                with len 2
197

198
        With this spec 'guarded_unpack_sequence' does something like this for
199
        protection (len checks are omitted):
200

201
            t = list(_getiter_(g))
202
            t[1] = list(_getiter_(t[1]))
203
            t[2] = list(_getiter_(t[2]))
204
            t[2][1] = list(_getiter_(t[2][1]))
205
            return t
206

207
        The 'real' spec for the case above is then:
208
            spec = {
209
                'min_len': 3,
210
                'childs': (
211
                    (1, {'min_len': 2, 'childs': ()}),
212
                    (2, {
213
                            'min_len': 2,
214
                            'childs': (
215
                                (1, {'min_len': 2, 'childs': ()})
216
                            )
217
                        }
218
                    )
219
                )
220
            }
221

222
        So finally the assignment above is converted into:
223
            (a, (b, c), (d, (e, f))) = guarded_unpack_sequence(g, spec)
224
        """
225
        spec = ast.Dict(keys=[], values=[])
1✔
226

227
        spec.keys.append(ast.Str('childs'))
1✔
228
        spec.values.append(ast.Tuple([], ast.Load()))
1✔
229

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

236
        for idx, val in enumerate(tpl.elts):
1✔
237
            # After a starred element specify the child index from the back.
238
            # Since it is unknown how many elements from the sequence are
239
            # consumed by the starred element.
240
            # For example a, *b, (c, d) = g
241
            # Then (c, d) has the index '-1'
242
            if self.is_starred(val):
1✔
243
                offset = min_len + 1
1✔
244

245
            elif isinstance(val, ast.Tuple):
1✔
246
                el = ast.Tuple([], ast.Load())
1✔
247
                el.elts.append(ast.Num(idx - offset))
1✔
248
                el.elts.append(self.gen_unpack_spec(val))
1✔
249
                spec.values[0].elts.append(el)
1✔
250

251
        spec.keys.append(ast.Str('min_len'))
1✔
252
        spec.values.append(ast.Num(min_len))
1✔
253

254
        return spec
1✔
255

256
    def protect_unpack_sequence(self, target, value):
1✔
257
        spec = self.gen_unpack_spec(target)
1✔
258
        return ast.Call(
1✔
259
            func=ast.Name('_unpack_sequence_', ast.Load()),
260
            args=[value, spec, ast.Name('_getiter_', ast.Load())],
261
            keywords=[])
262

263
    def gen_unpack_wrapper(self, node, target):
1✔
264
        """Helper function to protect tuple unpacks.
265

266
        node: used to copy the locations for the new nodes.
267
        target: is the tuple which must be protected.
268

269
        It returns a tuple with two element.
270

271
        Element 1: Is a temporary name node which must be used to
272
                   replace the target.
273
                   The context (store, param) is defined
274
                   by the 'ctx' parameter..
275

276
        Element 2: Is a try .. finally where the body performs the
277
                   protected tuple unpack of the temporary variable
278
                   into the original target.
279
        """
280

281
        # Generate a tmp name to replace the tuple with.
282
        tmp_name = self.gen_tmp_name()
1✔
283

284
        # Generates an expressions which protects the unpack.
285
        # converter looks like 'wrapper(tmp_name)'.
286
        # 'wrapper' takes care to protect sequence unpacking with _getiter_.
287
        converter = self.protect_unpack_sequence(
1✔
288
            target,
289
            ast.Name(tmp_name, ast.Load()))
290

291
        # Assign the expression to the original names.
292
        # Cleanup the temporary variable.
293
        # Generates:
294
        # try:
295
        #     # converter is 'wrapper(tmp_name)'
296
        #     arg = converter
297
        # finally:
298
        #     del tmp_arg
299
        try_body = [ast.Assign(targets=[target], value=converter)]
1✔
300
        finalbody = [self.gen_del_stmt(tmp_name)]
1✔
301
        cleanup = ast.Try(
1✔
302
            body=try_body, finalbody=finalbody, handlers=[], orelse=[])
303

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

307
        copy_locations(tmp_target, node)
1✔
308
        copy_locations(cleanup, node)
1✔
309

310
        return (tmp_target, cleanup)
1✔
311

312
    def gen_none_node(self):
1✔
313
        return ast.NameConstant(value=None)
1✔
314

315
    def gen_del_stmt(self, name_to_del):
1✔
316
        return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())])
1✔
317

318
    def transform_slice(self, slice_):
1✔
319
        """Transform slices into function parameters.
320

321
        ast.Slice nodes are only allowed within a ast.Subscript node.
322
        To use a slice as an argument of ast.Call it has to be converted.
323
        Conversion is done by calling the 'slice' function from builtins
324
        """
325

326
        if isinstance(slice_, ast.expr):
1!
327
            # Python 3.9+
328
            return slice_
×
329

330
        elif isinstance(slice_, ast.Index):
1✔
331
            return slice_.value
1✔
332

333
        elif isinstance(slice_, ast.Slice):
1✔
334
            # Create a python slice object.
335
            args = []
1✔
336

337
            if slice_.lower:
1✔
338
                args.append(slice_.lower)
1✔
339
            else:
340
                args.append(self.gen_none_node())
1✔
341

342
            if slice_.upper:
1✔
343
                args.append(slice_.upper)
1✔
344
            else:
345
                args.append(self.gen_none_node())
1✔
346

347
            if slice_.step:
1✔
348
                args.append(slice_.step)
1✔
349
            else:
350
                args.append(self.gen_none_node())
1✔
351

352
            return ast.Call(
1✔
353
                func=ast.Name('slice', ast.Load()),
354
                args=args,
355
                keywords=[])
356

357
        elif isinstance(slice_, ast.ExtSlice):
1✔
358
            dims = ast.Tuple([], ast.Load())
1✔
359
            for item in slice_.dims:
1✔
360
                dims.elts.append(self.transform_slice(item))
1✔
361
            return dims
1✔
362

363
        else:  # pragma: no cover
364
            # Index, Slice and ExtSlice are only defined Slice types.
365
            raise NotImplementedError(f"Unknown slice type: {slice_}")
366

367
    def check_name(self, node, name, allow_magic_methods=False):
1✔
368
        """Check names if they are allowed.
369

370
        If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES`
371
        are additionally allowed although their names start with `_`.
372

373
        """
374
        if name is None:
1✔
375
            return
1✔
376

377
        if (name.startswith('_')
1✔
378
                and name != '_'
379
                and not (allow_magic_methods
380
                         and name in ALLOWED_FUNC_NAMES
381
                         and node.col_offset != 0)):
382
            self.error(
1✔
383
                node,
384
                '"{name}" is an invalid variable name because it '
385
                'starts with "_"'.format(name=name))
386
        elif name.endswith('__roles__'):
1✔
387
            self.error(node, '"%s" is an invalid variable name because '
1✔
388
                       'it ends with "__roles__".' % name)
389
        elif name in FORBIDDEN_FUNC_NAMES:
1✔
390
            self.error(node, f'"{name}" is a reserved name.')
1✔
391

392
    def check_function_argument_names(self, node):
1✔
393
        for arg in node.args.args:
1✔
394
            self.check_name(node, arg.arg)
1✔
395

396
        if node.args.vararg:
1✔
397
            self.check_name(node, node.args.vararg.arg)
1✔
398

399
        if node.args.kwarg:
1✔
400
            self.check_name(node, node.args.kwarg.arg)
1✔
401

402
        for arg in node.args.kwonlyargs:
1✔
403
            self.check_name(node, arg.arg)
1✔
404

405
    def check_import_names(self, node):
1✔
406
        """Check the names being imported.
407

408
        This is a protection against rebinding dunder names like
409
        _getitem_, _write_ via imports.
410

411
        => 'from _a import x' is ok, because '_a' is not added to the scope.
412
        """
413
        for name in node.names:
1✔
414
            if '*' in name.name:
1✔
415
                self.error(node, '"*" imports are not allowed.')
1✔
416
            self.check_name(node, name.name)
1✔
417
            if name.asname:
1✔
418
                self.check_name(node, name.asname)
1✔
419

420
        return self.node_contents_visit(node)
1✔
421

422
    def inject_print_collector(self, node, position=0):
1✔
423
        print_used = self.print_info.print_used
1✔
424
        printed_used = self.print_info.printed_used
1✔
425

426
        if print_used or printed_used:
1✔
427
            # Add '_print = _print_(_getattr_)' add the top of a
428
            # function/module.
429
            _print = ast.Assign(
1✔
430
                targets=[ast.Name('_print', ast.Store())],
431
                value=ast.Call(
432
                    func=ast.Name("_print_", ast.Load()),
433
                    args=[ast.Name("_getattr_", ast.Load())],
434
                    keywords=[]))
435

436
            if isinstance(node, ast.Module):
1✔
437
                _print.lineno = position
1✔
438
                _print.col_offset = position
1✔
439
                if IS_PY38_OR_GREATER:
1!
440
                    _print.end_lineno = position
1✔
441
                    _print.end_col_offset = position
1✔
442
                ast.fix_missing_locations(_print)
1✔
443
            else:
444
                copy_locations(_print, node)
1✔
445

446
            node.body.insert(position, _print)
1✔
447

448
            if not printed_used:
1✔
449
                self.warn(node, "Prints, but never reads 'printed' variable.")
1✔
450

451
            elif not print_used:
1✔
452
                self.warn(node, "Doesn't print, but reads 'printed' variable.")
1✔
453

454
    # Special Functions for an ast.NodeTransformer
455

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

459
        This is needed to prevent new ast nodes from new Python versions to be
460
        trusted before any security review.
461

462
        To access `generic_visit` on the super class use `node_contents_visit`.
463
        """
464
        self.warn(
1✔
465
            node,
466
            '{0.__class__.__name__}'
467
            ' statement is not known to RestrictedPython'.format(node)
468
        )
469
        self.not_allowed(node)
1✔
470

471
    def not_allowed(self, node):
1✔
472
        self.error(
1✔
473
            node,
474
            f'{node.__class__.__name__} statements are not allowed.')
475

476
    def node_contents_visit(self, node):
1✔
477
        """Visit the contents of a node."""
478
        return super().generic_visit(node)
1✔
479

480
    # ast for Literals
481

482
    if IS_PY38_OR_GREATER:
1!
483

484
        def visit_Constant(self, node):
1✔
485
            """Allow constant literals with restriction for Ellipsis.
486

487
            Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in
488
            Python 3.8+.
489
            :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated
490
            """
491
            if node.value is Ellipsis:
1✔
492
                # Deny using `...`.
493
                # Special handling necessary as ``self.not_allowed(node)``
494
                # would return the Error Message:
495
                # 'Constant statements are not allowed.'
496
                # which is only partial true.
497
                self.error(node, 'Ellipsis statements are not allowed.')
1✔
498
                return
1✔
499
            return self.node_contents_visit(node)
1✔
500

501
    else:
502

503
        def visit_Num(self, node):
×
504
            """Allow integer numbers without restrictions.
505

506
            Replaced by Constant in Python 3.8.
507
            """
508
            return self.node_contents_visit(node)
×
509

510
        def visit_Str(self, node):
×
511
            """Allow string literals without restrictions.
512

513
            Replaced by Constant in Python 3.8.
514
            """
515
            return self.node_contents_visit(node)
×
516

517
        def visit_Bytes(self, node):
×
518
            """Allow bytes literals without restrictions.
519

520
            Replaced by Constant in Python 3.8.
521
            """
522
            return self.node_contents_visit(node)
×
523

524
        def visit_Ellipsis(self, node):
×
525
            """Deny using `...`.
526

527
            Replaced by Constant in Python 3.8.
528
            """
529
            return self.not_allowed(node)
×
530

531
        def visit_NameConstant(self, node):
×
532
            """Allow constant literals (True, False, None) without ...
533

534
            restrictions.
535

536
            Replaced by Constant in Python 3.8.
537
            """
538
            return self.node_contents_visit(node)
×
539

540
    def visit_List(self, node):
1✔
541
        """Allow list literals without restrictions."""
542
        return self.node_contents_visit(node)
1✔
543

544
    def visit_Tuple(self, node):
1✔
545
        """Allow tuple literals without restrictions."""
546
        return self.node_contents_visit(node)
1✔
547

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

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

556
    def visit_FormattedValue(self, node):
1✔
557
        """Allow f-strings without restrictions."""
558
        return self.node_contents_visit(node)
1✔
559

560
    def visit_JoinedStr(self, node):
1✔
561
        """Allow joined string without restrictions."""
562
        return self.node_contents_visit(node)
1✔
563

564
    # ast for Variables
565

566
    def visit_Name(self, node):
1✔
567
        """Prevents access to protected names.
568

569
        Converts use of the name 'printed' to this expression: '_print()'
570
        """
571

572
        node = self.node_contents_visit(node)
1✔
573

574
        if isinstance(node.ctx, ast.Load):
1✔
575
            if node.id == 'printed':
1✔
576
                self.print_info.printed_used = True
1✔
577
                new_node = ast.Call(
1✔
578
                    func=ast.Name("_print", ast.Load()),
579
                    args=[],
580
                    keywords=[])
581

582
                copy_locations(new_node, node)
1✔
583
                return new_node
1✔
584

585
            elif node.id == 'print':
1✔
586
                self.print_info.print_used = True
1✔
587
                new_node = ast.Attribute(
1✔
588
                    value=ast.Name('_print', ast.Load()),
589
                    attr="_call_print",
590
                    ctx=ast.Load())
591

592
                copy_locations(new_node, node)
1✔
593
                return new_node
1✔
594

595
            self.used_names[node.id] = True
1✔
596

597
        self.check_name(node, node.id)
1✔
598
        return node
1✔
599

600
    def visit_Load(self, node):
1✔
601
        """
602

603
        """
604
        return self.node_contents_visit(node)
1✔
605

606
    def visit_Store(self, node):
1✔
607
        """
608

609
        """
610
        return self.node_contents_visit(node)
1✔
611

612
    def visit_Del(self, node):
1✔
613
        """
614

615
        """
616
        return self.node_contents_visit(node)
1✔
617

618
    def visit_Starred(self, node):
1✔
619
        """
620

621
        """
622
        return self.node_contents_visit(node)
1✔
623

624
    # Expressions
625

626
    def visit_Expression(self, node):
1✔
627
        """Allow Expression statements without restrictions.
628

629
        They are in the AST when using the `eval` compile mode.
630
        """
631
        return self.node_contents_visit(node)
1✔
632

633
    def visit_Expr(self, node):
1✔
634
        """Allow Expr statements (any expression) without restrictions."""
635
        return self.node_contents_visit(node)
1✔
636

637
    def visit_UnaryOp(self, node):
1✔
638
        """
639
        UnaryOp (Unary Operations) is the overall element for:
640
        * Not --> which should be allowed
641
        * UAdd --> Positive notation of variables (e.g. +var)
642
        * USub --> Negative notation of variables (e.g. -var)
643
        """
644
        return self.node_contents_visit(node)
1✔
645

646
    def visit_UAdd(self, node):
1✔
647
        """Allow positive notation of variables. (e.g. +var)"""
648
        return self.node_contents_visit(node)
1✔
649

650
    def visit_USub(self, node):
1✔
651
        """Allow negative notation of variables. (e.g. -var)"""
652
        return self.node_contents_visit(node)
1✔
653

654
    def visit_Not(self, node):
1✔
655
        """Allow the `not` operator."""
656
        return self.node_contents_visit(node)
1✔
657

658
    def visit_Invert(self, node):
1✔
659
        """Allow `~` expressions."""
660
        return self.node_contents_visit(node)
1✔
661

662
    def visit_BinOp(self, node):
1✔
663
        """Allow binary operations."""
664
        return self.node_contents_visit(node)
1✔
665

666
    def visit_Add(self, node):
1✔
667
        """Allow `+` expressions."""
668
        return self.node_contents_visit(node)
1✔
669

670
    def visit_Sub(self, node):
1✔
671
        """Allow `-` expressions."""
672
        return self.node_contents_visit(node)
1✔
673

674
    def visit_Mult(self, node):
1✔
675
        """Allow `*` expressions."""
676
        return self.node_contents_visit(node)
1✔
677

678
    def visit_Div(self, node):
1✔
679
        """Allow `/` expressions."""
680
        return self.node_contents_visit(node)
1✔
681

682
    def visit_FloorDiv(self, node):
1✔
683
        """Allow `//` expressions."""
684
        return self.node_contents_visit(node)
1✔
685

686
    def visit_Mod(self, node):
1✔
687
        """Allow `%` expressions."""
688
        return self.node_contents_visit(node)
1✔
689

690
    def visit_Pow(self, node):
1✔
691
        """Allow `**` expressions."""
692
        return self.node_contents_visit(node)
1✔
693

694
    def visit_LShift(self, node):
1✔
695
        """Allow `<<` expressions."""
696
        return self.node_contents_visit(node)
1✔
697

698
    def visit_RShift(self, node):
1✔
699
        """Allow `>>` expressions."""
700
        return self.node_contents_visit(node)
1✔
701

702
    def visit_BitOr(self, node):
1✔
703
        """Allow `|` expressions."""
704
        return self.node_contents_visit(node)
1✔
705

706
    def visit_BitXor(self, node):
1✔
707
        """Allow `^` expressions."""
708
        return self.node_contents_visit(node)
1✔
709

710
    def visit_BitAnd(self, node):
1✔
711
        """Allow `&` expressions."""
712
        return self.node_contents_visit(node)
1✔
713

714
    def visit_MatMult(self, node):
1✔
715
        """Matrix multiplication (`@`) is currently not allowed."""
716
        self.not_allowed(node)
1✔
717

718
    def visit_BoolOp(self, node):
1✔
719
        """Allow bool operator without restrictions."""
720
        return self.node_contents_visit(node)
1✔
721

722
    def visit_And(self, node):
1✔
723
        """Allow bool operator `and` without restrictions."""
724
        return self.node_contents_visit(node)
1✔
725

726
    def visit_Or(self, node):
1✔
727
        """Allow bool operator `or` without restrictions."""
728
        return self.node_contents_visit(node)
1✔
729

730
    def visit_Compare(self, node):
1✔
731
        """Allow comparison expressions without restrictions."""
732
        return self.node_contents_visit(node)
1✔
733

734
    def visit_Eq(self, node):
1✔
735
        """Allow == expressions."""
736
        return self.node_contents_visit(node)
1✔
737

738
    def visit_NotEq(self, node):
1✔
739
        """Allow != expressions."""
740
        return self.node_contents_visit(node)
1✔
741

742
    def visit_Lt(self, node):
1✔
743
        """Allow < expressions."""
744
        return self.node_contents_visit(node)
1✔
745

746
    def visit_LtE(self, node):
1✔
747
        """Allow <= expressions."""
748
        return self.node_contents_visit(node)
1✔
749

750
    def visit_Gt(self, node):
1✔
751
        """Allow > expressions."""
752
        return self.node_contents_visit(node)
1✔
753

754
    def visit_GtE(self, node):
1✔
755
        """Allow >= expressions."""
756
        return self.node_contents_visit(node)
1✔
757

758
    def visit_Is(self, node):
1✔
759
        """Allow `is` expressions."""
760
        return self.node_contents_visit(node)
1✔
761

762
    def visit_IsNot(self, node):
1✔
763
        """Allow `is not` expressions."""
764
        return self.node_contents_visit(node)
1✔
765

766
    def visit_In(self, node):
1✔
767
        """Allow `in` expressions."""
768
        return self.node_contents_visit(node)
1✔
769

770
    def visit_NotIn(self, node):
1✔
771
        """Allow `not in` expressions."""
772
        return self.node_contents_visit(node)
1✔
773

774
    def visit_Call(self, node):
1✔
775
        """Checks calls with '*args' and '**kwargs'.
776

777
        Note: The following happens only if '*args' or '**kwargs' is used.
778

779
        Transfroms 'foo(<all the possible ways of args>)' into
780
        _apply_(foo, <all the possible ways for args>)
781

782
        The thing is that '_apply_' has only '*args', '**kwargs', so it gets
783
        Python to collapse all the myriad ways to call functions
784
        into one manageable from.
785

786
        From there, '_apply_()' wraps args and kws in guarded accessors,
787
        then calls the function, returning the value.
788
        """
789

790
        if isinstance(node.func, ast.Name):
1✔
791
            if node.func.id == 'exec':
1✔
792
                self.error(node, 'Exec calls are not allowed.')
1✔
793
            elif node.func.id == 'eval':
1✔
794
                self.error(node, 'Eval calls are not allowed.')
1✔
795

796
        needs_wrap = False
1✔
797

798
        for pos_arg in node.args:
1✔
799
            if isinstance(pos_arg, ast.Starred):
1✔
800
                needs_wrap = True
1✔
801

802
        for keyword_arg in node.keywords:
1✔
803
            if keyword_arg.arg is None:
1✔
804
                needs_wrap = True
1✔
805

806
        node = self.node_contents_visit(node)
1✔
807

808
        if not needs_wrap:
1✔
809
            return node
1✔
810

811
        node.args.insert(0, node.func)
1✔
812
        node.func = ast.Name('_apply_', ast.Load())
1✔
813
        copy_locations(node.func, node.args[0])
1✔
814
        return node
1✔
815

816
    def visit_keyword(self, node):
1✔
817
        """
818

819
        """
820
        return self.node_contents_visit(node)
1✔
821

822
    def visit_IfExp(self, node):
1✔
823
        """Allow `if` expressions without restrictions."""
824
        return self.node_contents_visit(node)
1✔
825

826
    def visit_Attribute(self, node):
1✔
827
        """Checks and mutates attribute access/assignment.
828

829
        'a.b' becomes '_getattr_(a, "b")'
830
        'a.b = c' becomes '_write_(a).b = c'
831
        'del a.b' becomes 'del _write_(a).b'
832

833
        The _write_ function should return a security proxy.
834
        """
835
        if node.attr.startswith('_') and node.attr != '_':
1✔
836
            self.error(
1✔
837
                node,
838
                '"{name}" is an invalid attribute name because it starts '
839
                'with "_".'.format(name=node.attr))
840

841
        if node.attr.endswith('__roles__'):
1✔
842
            self.error(
1✔
843
                node,
844
                '"{name}" is an invalid attribute name because it ends '
845
                'with "__roles__".'.format(name=node.attr))
846

847
        if isinstance(node.ctx, ast.Load):
1✔
848
            node = self.node_contents_visit(node)
1✔
849
            new_node = ast.Call(
1✔
850
                func=ast.Name('_getattr_', ast.Load()),
851
                args=[node.value, ast.Str(node.attr)],
852
                keywords=[])
853

854
            copy_locations(new_node, node)
1✔
855
            return new_node
1✔
856

857
        elif isinstance(node.ctx, (ast.Store, ast.Del)):
1✔
858
            node = self.node_contents_visit(node)
1✔
859
            new_value = ast.Call(
1✔
860
                func=ast.Name('_write_', ast.Load()),
861
                args=[node.value],
862
                keywords=[])
863

864
            copy_locations(new_value, node.value)
1✔
865
            node.value = new_value
1✔
866
            return node
1✔
867

868
        else:  # pragma: no cover
869
            # Impossible Case only ctx Load, Store and Del are defined in ast.
870
            raise NotImplementedError(
871
                f"Unknown ctx type: {type(node.ctx)}")
872

873
    # Subscripting
874

875
    def visit_Subscript(self, node):
1✔
876
        """Transforms all kinds of subscripts.
877

878
        'foo[bar]' becomes '_getitem_(foo, bar)'
879
        'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
880
        'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
881
        'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
882
        'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
883
        'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
884
        'foo[a] = c' becomes '_write_(foo)[a] = c'
885
        'del foo[a]' becomes 'del _write_(foo)[a]'
886

887
        The _write_ function should return a security proxy.
888
        """
889
        node = self.node_contents_visit(node)
1✔
890

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

896
        if isinstance(node.ctx, ast.Load):
1✔
897
            new_node = ast.Call(
1✔
898
                func=ast.Name('_getitem_', ast.Load()),
899
                args=[node.value, self.transform_slice(node.slice)],
900
                keywords=[])
901

902
            copy_locations(new_node, node)
1✔
903
            return new_node
1✔
904

905
        elif isinstance(node.ctx, (ast.Del, ast.Store)):
1✔
906
            new_value = ast.Call(
1✔
907
                func=ast.Name('_write_', ast.Load()),
908
                args=[node.value],
909
                keywords=[])
910

911
            copy_locations(new_value, node)
1✔
912
            node.value = new_value
1✔
913
            return node
1✔
914

915
        else:  # pragma: no cover
916
            # Impossible Case only ctx Load, Store and Del are defined in ast.
917
            raise NotImplementedError(
918
                f"Unknown ctx type: {type(node.ctx)}")
919

920
    def visit_Index(self, node):
1✔
921
        """
922

923
        """
924
        return self.node_contents_visit(node)
1✔
925

926
    def visit_Slice(self, node):
1✔
927
        """
928

929
        """
930
        return self.node_contents_visit(node)
1✔
931

932
    def visit_ExtSlice(self, node):
1✔
933
        """
934

935
        """
936
        return self.node_contents_visit(node)
1✔
937

938
    # Comprehensions
939

940
    def visit_ListComp(self, node):
1✔
941
        """
942

943
        """
944
        return self.node_contents_visit(node)
1✔
945

946
    def visit_SetComp(self, node):
1✔
947
        """
948

949
        """
950
        return self.node_contents_visit(node)
1✔
951

952
    def visit_GeneratorExp(self, node):
1✔
953
        """
954

955
        """
956
        return self.node_contents_visit(node)
1✔
957

958
    def visit_DictComp(self, node):
1✔
959
        """
960

961
        """
962
        return self.node_contents_visit(node)
1✔
963

964
    def visit_comprehension(self, node):
1✔
965
        """
966

967
        """
968
        return self.guard_iter(node)
1✔
969

970
    # Statements
971

972
    def visit_Assign(self, node):
1✔
973
        """
974

975
        """
976

977
        node = self.node_contents_visit(node)
1✔
978

979
        if not any(isinstance(t, ast.Tuple) for t in node.targets):
1✔
980
            return node
1✔
981

982
        # Handle sequence unpacking.
983
        # For briefness this example omits cleanup of the temporary variables.
984
        # Check 'transform_tuple_assign' how its done.
985
        #
986
        # - Single target (with nested support)
987
        # (a, (b, (c, d))) = <exp>
988
        # is converted to
989
        # (a, t1) = _getiter_(<exp>)
990
        # (b, t2) = _getiter_(t1)
991
        # (c, d) = _getiter_(t2)
992
        #
993
        # - Multi targets
994
        # (a, b) = (c, d) = <exp>
995
        # is converted to
996
        # (c, d) = _getiter_(<exp>)
997
        # (a, b) = _getiter_(<exp>)
998
        # Why is this valid ? The original bytecode for this multi targets
999
        # behaves the same way.
1000

1001
        # ast.NodeTransformer works with list results.
1002
        # He injects it at the right place of the node's parent statements.
1003
        new_nodes = []
1✔
1004

1005
        # python fills the right most target first.
1006
        for target in reversed(node.targets):
1✔
1007
            if isinstance(target, ast.Tuple):
1✔
1008
                wrapper = ast.Assign(
1✔
1009
                    targets=[target],
1010
                    value=self.protect_unpack_sequence(target, node.value))
1011
                new_nodes.append(wrapper)
1✔
1012
            else:
1013
                new_node = ast.Assign(targets=[target], value=node.value)
1✔
1014
                new_nodes.append(new_node)
1✔
1015

1016
        for new_node in new_nodes:
1✔
1017
            copy_locations(new_node, node)
1✔
1018

1019
        return new_nodes
1✔
1020

1021
    def visit_AugAssign(self, node):
1✔
1022
        """Forbid certain kinds of AugAssign
1023

1024
        According to the language reference (and ast.c) the following nodes
1025
        are are possible:
1026
        Name, Attribute, Subscript
1027

1028
        Note that although augmented assignment of attributes and
1029
        subscripts is disallowed, augmented assignment of names (such
1030
        as 'n += 1') is allowed.
1031
        'n += 1' becomes 'n = _inplacevar_("+=", n, 1)'
1032
        """
1033

1034
        node = self.node_contents_visit(node)
1✔
1035

1036
        if isinstance(node.target, ast.Attribute):
1✔
1037
            self.error(
1✔
1038
                node,
1039
                "Augmented assignment of attributes is not allowed.")
1040
            return node
1✔
1041

1042
        elif isinstance(node.target, ast.Subscript):
1✔
1043
            self.error(
1✔
1044
                node,
1045
                "Augmented assignment of object items "
1046
                "and slices is not allowed.")
1047
            return node
1✔
1048

1049
        elif isinstance(node.target, ast.Name):
1✔
1050
            new_node = ast.Assign(
1✔
1051
                targets=[node.target],
1052
                value=ast.Call(
1053
                    func=ast.Name('_inplacevar_', ast.Load()),
1054
                    args=[
1055
                        ast.Str(IOPERATOR_TO_STR[type(node.op)]),
1056
                        ast.Name(node.target.id, ast.Load()),
1057
                        node.value
1058
                    ],
1059
                    keywords=[]))
1060

1061
            copy_locations(new_node, node)
1✔
1062
            return new_node
1✔
1063
        else:  # pragma: no cover
1064
            # Impossible Case - Only Node Types:
1065
            # * Name
1066
            # * Attribute
1067
            # * Subscript
1068
            # defined, those are checked before.
1069
            raise NotImplementedError(
1070
                f"Unknown target type: {type(node.target)}")
1071

1072
    def visit_Raise(self, node):
1✔
1073
        """Allow `raise` statements without restrictions."""
1074
        return self.node_contents_visit(node)
1✔
1075

1076
    def visit_Assert(self, node):
1✔
1077
        """Allow assert statements without restrictions."""
1078
        return self.node_contents_visit(node)
1✔
1079

1080
    def visit_Delete(self, node):
1✔
1081
        """Allow `del` statements without restrictions."""
1082
        return self.node_contents_visit(node)
1✔
1083

1084
    def visit_Pass(self, node):
1✔
1085
        """Allow `pass` statements without restrictions."""
1086
        return self.node_contents_visit(node)
1✔
1087

1088
    # Imports
1089

1090
    def visit_Import(self, node):
1✔
1091
        """Allow `import` statements with restrictions.
1092
        See check_import_names."""
1093
        return self.check_import_names(node)
1✔
1094

1095
    def visit_ImportFrom(self, node):
1✔
1096
        """Allow `import from` statements with restrictions.
1097
        See check_import_names."""
1098
        return self.check_import_names(node)
1✔
1099

1100
    def visit_alias(self, node):
1✔
1101
        """Allow `as` statements in import and import from statements."""
1102
        return self.node_contents_visit(node)
1✔
1103

1104
    # Control flow
1105

1106
    def visit_If(self, node):
1✔
1107
        """Allow `if` statements without restrictions."""
1108
        return self.node_contents_visit(node)
1✔
1109

1110
    def visit_For(self, node):
1✔
1111
        """Allow `for` statements with some restrictions."""
1112
        return self.guard_iter(node)
1✔
1113

1114
    def visit_While(self, node):
1✔
1115
        """Allow `while` statements."""
1116
        return self.node_contents_visit(node)
1✔
1117

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

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

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

1130
    def visit_TryStar(self, node):
1✔
1131
        """Allow `ExceptionGroup` without restrictions."""
1132
        return self.node_contents_visit(node)
×
1133

1134
    def visit_ExceptHandler(self, node):
1✔
1135
        """Protect exception handlers."""
1136
        node = self.node_contents_visit(node)
1✔
1137
        self.check_name(node, node.name)
1✔
1138
        return node
1✔
1139

1140
    def visit_With(self, node):
1✔
1141
        """Protect tuple unpacking on with statements."""
1142
        node = self.node_contents_visit(node)
1✔
1143

1144
        for item in reversed(node.items):
1✔
1145
            if isinstance(item.optional_vars, ast.Tuple):
1✔
1146
                tmp_target, unpack = self.gen_unpack_wrapper(
1✔
1147
                    node,
1148
                    item.optional_vars)
1149

1150
                item.optional_vars = tmp_target
1✔
1151
                node.body.insert(0, unpack)
1✔
1152

1153
        return node
1✔
1154

1155
    def visit_withitem(self, node):
1✔
1156
        """Allow `with` statements (context managers) without restrictions."""
1157
        return self.node_contents_visit(node)
1✔
1158

1159
    # Function and class definitions
1160

1161
    def visit_FunctionDef(self, node):
1✔
1162
        """Allow function definitions (`def`) with some restrictions."""
1163
        self.check_name(node, node.name, allow_magic_methods=True)
1✔
1164
        self.check_function_argument_names(node)
1✔
1165

1166
        with self.print_info.new_print_scope():
1✔
1167
            node = self.node_contents_visit(node)
1✔
1168
            self.inject_print_collector(node)
1✔
1169
        return node
1✔
1170

1171
    def visit_Lambda(self, node):
1✔
1172
        """Allow lambda with some restrictions."""
1173
        self.check_function_argument_names(node)
1✔
1174
        return self.node_contents_visit(node)
1✔
1175

1176
    def visit_arguments(self, node):
1✔
1177
        """
1178

1179
        """
1180
        return self.node_contents_visit(node)
1✔
1181

1182
    def visit_arg(self, node):
1✔
1183
        """
1184

1185
        """
1186
        return self.node_contents_visit(node)
1✔
1187

1188
    def visit_Return(self, node):
1✔
1189
        """Allow `return` statements without restrictions."""
1190
        return self.node_contents_visit(node)
1✔
1191

1192
    def visit_Yield(self, node):
1✔
1193
        """Allow `yield`statements without restrictions."""
1194
        return self.node_contents_visit(node)
1✔
1195

1196
    def visit_YieldFrom(self, node):
1✔
1197
        """Allow `yield`statements without restrictions."""
1198
        return self.node_contents_visit(node)
1✔
1199

1200
    def visit_Global(self, node):
1✔
1201
        """Allow `global` statements without restrictions."""
1202
        return self.node_contents_visit(node)
1✔
1203

1204
    def visit_Nonlocal(self, node):
1✔
1205
        """Deny `nonlocal` statements."""
1206
        self.not_allowed(node)
1✔
1207

1208
    def visit_ClassDef(self, node):
1✔
1209
        """Check the name of a class definition."""
1210
        self.check_name(node, node.name)
1✔
1211
        node = self.node_contents_visit(node)
1✔
1212
        if any(keyword.arg == 'metaclass' for keyword in node.keywords):
1✔
1213
            self.error(
1✔
1214
                node, 'The keyword argument "metaclass" is not allowed.')
1215
        CLASS_DEF = textwrap.dedent('''\
1✔
1216
            class {0.name}(metaclass=__metaclass__):
1217
                pass
1218
        '''.format(node))
1219
        new_class_node = ast.parse(CLASS_DEF).body[0]
1✔
1220
        new_class_node.body = node.body
1✔
1221
        new_class_node.bases = node.bases
1✔
1222
        new_class_node.decorator_list = node.decorator_list
1✔
1223
        return new_class_node
1✔
1224

1225
    def visit_Module(self, node):
1✔
1226
        """Add the print_collector (only if print is used) at the top."""
1227
        node = self.node_contents_visit(node)
1✔
1228

1229
        # Inject the print collector after 'from __future__ import ....'
1230
        position = 0
1✔
1231
        for position, child in enumerate(node.body):  # pragma: no branch
1✔
1232
            if not isinstance(child, ast.ImportFrom):
1✔
1233
                break
1✔
1234

1235
            if not child.module == '__future__':
1✔
1236
                break
1✔
1237

1238
        self.inject_print_collector(node, position)
1✔
1239
        return node
1✔
1240

1241
    # Async und await
1242

1243
    def visit_AsyncFunctionDef(self, node):
1✔
1244
        """Deny async functions."""
1245
        self.not_allowed(node)
1✔
1246

1247
    def visit_Await(self, node):
1✔
1248
        """Deny async functionality."""
1249
        self.not_allowed(node)
1✔
1250

1251
    def visit_AsyncFor(self, node):
1✔
1252
        """Deny async functionality."""
1253
        self.not_allowed(node)
1✔
1254

1255
    def visit_AsyncWith(self, node):
1✔
1256
        """Deny async functionality."""
1257
        self.not_allowed(node)
1✔
1258

1259
    # Assignment expressions (walrus operator ``:=``)
1260
    # New in 3.8
1261
    def visit_NamedExpr(self, node):
1✔
1262
        """Allow assignment expressions under some circumstances."""
1263
        # while the grammar requires ``node.target`` to be a ``Name``
1264
        # the abstract syntax is more permissive and allows an ``expr``.
1265
        # We support only a ``Name``.
1266
        # This is safe as the expression can only add/modify local
1267
        # variables. While this may hide global variables, an
1268
        # (implicitly performed) name check guarantees (as usual)
1269
        # that no essential global variable is hidden.
1270
        node = self.node_contents_visit(node)  # this checks ``node.target``
1✔
1271
        target = node.target
1✔
1272
        if not isinstance(target, ast.Name):
1✔
1273
            self.error(
1✔
1274
                node,
1275
                "Assignment expressions are only allowed for simple targets")
1276
        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