Safely eval Python Syntax

using the AST module


Use Cases

  • Expression matching events
  • Provide a query syntax
    • Create your own "DSL"

The Dangers of eval & exec

  • Standard advice is to avoid eval
  • evalhas valid uses

Why is eval dangerous?

  • Can provide our own locals and globals dicts
    
    eval(supplied_syntax, {}, {})
    
  • But Python is Meta!
    
    ().__class__.__base__.__subclasses__()
    

Lets Do Something Nasty...


syntax = """
[
    s for s in ().__class__.__base__.__subclasses__() 
    if s.__name__ == 'Quitter'
][0]('', 0)()
"""
eval(syntax, {}, {})

More info on eval

See Ned Batchelders excellent talk on the topic.

http://bit.ly/2ZKGWcl https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html

Use the AST

  • In the Standard Library
  • Creates a syntax tree
  • Used by your favourite tools (pytest, black, bandit)

What does a Syntax tree look like


>>> ast.parse("obj.x == 123", mode="eval")

Expression(
    body=Compare(
        left=Attribute(
            value=Name(id='obj', ctx=Load()), 
            attr='x', 
            ctx=Load()
        ), 
        ops=[Eq()], 
        comparators=[Num(n=123)]
    )
)

ppast thanks to greentreesnakes.readthedocs.io

Nodes

  • Literals eg "foo", 123, True, None
  • Variables
  • Expressions eg Operators, comparison, call , subscripting, comprehensions
  • Statements assignment, print, raise, del, import...
  • Control flow if/else, try/except, for, while, with...
  • Function and Class Def, Async

What to Allow?

Depends on your application!

  • Only allow a single expression, use eval mode
    ast.parse(expression, mode="eval")
  • Filter any ast.Call instances eg
    call.func.id in ("any", "all")
  • Not allow dunders
    not attr.attr.startswith("__")

Code example!

Back to eval

  • Just validate syntax

What else can you do?

  • Edit the AST
  • Generate our own code

Useful things we can do

  • Define our own DSL?
  • Tweak behaviour
  • Apply fine-grained permissions

Enhancing an expression

# The expression
left.name == right.name

# With an extra check
(left.name is not None) and (left.name == right.name)

Permissions

has_permission(left, "name") and (left.name == right.name)

Question/Comments

  • Email: tim@savage.company
  • Twitter: @savage_drummer
  • Github: github.com/timsavage
  • PyPI: odin or pyapp