Velocity Reviews - Computer Hardware Reviews

Velocity Reviews > Newsgroups > Programming > Python > Re: Simple and safe evaluator

Reply
Thread Tools

Re: Simple and safe evaluator

 
 
bvdp
Guest
Posts: n/a
 
      06-11-2008

I'm finding my quest for a safe eval() quite frustrating

Any comments on this: Just forget about getting python to do this and,
instead, grab my set of values (from a user supplied text file) and call
an external program like 'bc' to do the dirty work. I think that this
would avoid someone from embedding os.system("rm ...") in what I thought
would be a math expression and having it maybe do damage? Perhaps I'm
getting too paranoid in my old age.

I guess this would slow things down a bit, but that is not a big
concern. Bigger concern would be that I'm not sure if 'bc' or whatever
is guaranteed to be on other platforms than *nix. And if I want to be
really paranoid, I could worry that someone had planted a bad 'bc' on
the target.
 
Reply With Quote
 
 
 
 
Matimus
Guest
Posts: n/a
 
      06-12-2008
On Jun 11, 4:38 pm, bvdp <b...@mellowood.ca> wrote:
> I'm finding my quest for a safe eval() quite frustrating
>
> Any comments on this: Just forget about getting python to do this and,
> instead, grab my set of values (from a user supplied text file) and call
> an external program like 'bc' to do the dirty work. I think that this
> would avoid someone from embedding os.system("rm ...") in what I thought
> would be a math expression and having it maybe do damage? Perhaps I'm
> getting too paranoid in my old age.
>
> I guess this would slow things down a bit, but that is not a big
> concern. Bigger concern would be that I'm not sure if 'bc' or whatever
> is guaranteed to be on other platforms than *nix. And if I want to be
> really paranoid, I could worry that someone had planted a bad 'bc' on
> the target.


The solution I posted should work and is safe. It may not seem very
readable, but it is using Pythons internal parser to parse the passed
in string into an abstract symbol tree (rather than code). Normally
Python would just use the ast internally to create code. Instead I've
written the code to do that. By avoiding anything but simple operators
and literals it is guaranteed safe.

If you only want numbers you can even remove a bunch of code:
Code:
import _ast

class SafeEvalError(Exception):
    pass

class UnsafeCode(SafeEvalError):
    pass

def num_eval(text):
    "similar to eval, but only works on numerical values."
    ast = compile(text, "<string>", 'eval', _ast.PyCF_ONLY_AST)
    return _traverse(ast.body)

def _traverse(ast):

    if isinstance(ast, _ast.Num):
        return ast.n
    elif isinstance(ast, _ast.Expr):
        return _traverse(ast.value)
    elif isinstance(ast, _ast.BinOp):
        if isinstance(ast.op, _ast.Add):
            return _traverse(ast.left) + _traverse(ast.right)
        elif isinstance(ast.op, _ast.Sub):
            return _traverse(ast.left) - _traverse(ast.right)
        elif isinstance(ast.op, _ast.Div):
            return _traverse(ast.left) / _traverse(ast.right)
        elif isinstance(ast.op, _ast.FloorDiv):
            return _traverse(ast.left) // _traverse(ast.right)
        elif isinstance(ast.op, _ast.Mod):
            return _traverse(ast.left) % _traverse(ast.right)
        elif isinstance(ast.op, _ast.Mult):
            return _traverse(ast.left) * _traverse(ast.right)
        elif isinstance(ast.op, _ast.Pow):
            return _traverse(ast.left) ** _traverse(ast.right)
        elif isinstance(ast.op, _ast.BitAnd):
            return _traverse(ast.left) & _traverse(ast.right)
        elif isinstance(ast.op, _ast.BitOr):
            return _traverse(ast.left) | _traverse(ast.right)
        elif isinstance(ast.op, _ast.BitXor):
            return _traverse(ast.left) ^ _traverse(ast.right)
        elif isinstance(ast.op, _ast.LShift):
            return _traverse(ast.left) << _traverse(ast.right)
        elif isinstance(ast.op, _ast.RShift):
            return _traverse(ast.left) >> _traverse(ast.right)
    elif isinstance(ast, _ast.UnaryOp):
        if isinstance(ast.op, _ast.Invert):
            return ~_traverse(ast.operand)
        elif isinstance(ast.op, _ast.USub):
            return -_traverse(ast.operand)
        elif isinstance(ast.op, _ast.UAdd):
            return +_traverse(ast.operand)

    raise UnsafeCode()
To use:
print num_eval("1 + 44 / 3")
 
Reply With Quote
 
 
 
 
bvdp
Guest
Posts: n/a
 
      06-12-2008
Matimus wrote:

>
> The solution I posted should work and is safe. It may not seem very
> readable, but it is using Pythons internal parser to parse the passed
> in string into an abstract symbol tree (rather than code). Normally
> Python would just use the ast internally to create code. Instead I've
> written the code to do that. By avoiding anything but simple operators
> and literals it is guaranteed safe.
>


Just wondering ... how safe would:

eval(s, {"__builtins__":None}, {} )

be? From my testing it seems that it parses out numbers properly (int
and float) and does simple math like +, -, **, etc. It doesn't do
functions like int(), sin(), etc ... but that is fine for my puposes.

Just playing a bit, it seems to give the same results as your code using
ast does. I may be missing something!
 
Reply With Quote
 
George Sakkis
Guest
Posts: n/a
 
      06-12-2008
On Jun 11, 8:15*pm, bvdp <b...@mellowood.ca> wrote:

> Matimus wrote:
>
> > The solution I posted should work and is safe. It may not seem very
> > readable, but it is using Pythons internal parser to parse the passed
> > in string into an abstract symbol tree (rather than code). Normally
> > Python would just use the ast internally to create code. Instead I've
> > written the code to do that. By avoiding anything but simple operators
> > and literals it is guaranteed safe.

>
> Just wondering ... how safe would:
>
> * * * * *eval(s, {"__builtins__":None}, {} )
>
> be? From my testing it seems that it parses out numbers properly (int
> and float) and does simple math like +, -, **, etc. It doesn't do
> functions like int(), sin(), etc ... but that is fine for my puposes.
>
> Just playing a bit, it seems to give the same results as your code using
> ast does. I may be missing something!


Probably you do; within a couple of minutes I came up with this:

>>> s = """

... (t for t in 42 .__class__.__base__.__subclasses__()
... if t.__name__ == 'file').next()('/etc/passwd')
... """
>>> eval(s, {"__builtins__":None}, {} )

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 3, in <module>
IOError: file() constructor not accessible in restricted mode


Not an exploit yet but I wouldn't be surprised if there is one. Unless
you fully trust your users, an ast-based approach is your best bet.

George

George
 
Reply With Quote
 
Matimus
Guest
Posts: n/a
 
      06-12-2008
On Jun 11, 9:16 pm, George Sakkis <george.sak...@gmail.com> wrote:
> On Jun 11, 8:15 pm, bvdp <b...@mellowood.ca> wrote:
>
>
>
> > Matimus wrote:

>
> > > The solution I posted should work and is safe. It may not seem very
> > > readable, but it is using Pythons internal parser to parse the passed
> > > in string into an abstract symbol tree (rather than code). Normally
> > > Python would just use the ast internally to create code. Instead I've
> > > written the code to do that. By avoiding anything but simple operators
> > > and literals it is guaranteed safe.

>
> > Just wondering ... how safe would:

>
> > eval(s, {"__builtins__":None}, {} )

>
> > be? From my testing it seems that it parses out numbers properly (int
> > and float) and does simple math like +, -, **, etc. It doesn't do
> > functions like int(), sin(), etc ... but that is fine for my puposes.

>
> > Just playing a bit, it seems to give the same results as your code using
> > ast does. I may be missing something!

>
> Probably you do; within a couple of minutes I came up with this:
>
> >>> s = """

>
> ... (t for t in 42 .__class__.__base__.__subclasses__()
> ... if t.__name__ == 'file').next()('/etc/passwd')
> ... """>>> eval(s, {"__builtins__":None}, {} )
>
> Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
> File "<string>", line 3, in <module>
> IOError: file() constructor not accessible in restricted mode
>
> Not an exploit yet but I wouldn't be surprised if there is one. Unless
> you fully trust your users, an ast-based approach is your best bet.
>
> George


You can get access to any new-style class that has been loaded. This
exploit works on my machine (Windows XP).

Code:
# This assumes that ctypes was loaded, but keep in mind any classes
# that have been loaded are potentially accessible.

import ctypes

s = """
(
    t for t in 42 .__class__.__base__.__subclasses__()
        if t.__name__ == 'LibraryLoader'
    ).next()(
        (
            t for t in 42 .__class__.__base__.__subclasses__()
                if t.__name__ == 'CDLL'
            ).next()
        ).msvcrt.system('dir') # replace 'dir' with something nasty
"""

eval(s, {"__builtins__":None}, {})
Matt
 
Reply With Quote
 
bvdp
Guest
Posts: n/a
 
      06-12-2008
Matimus wrote:
> On Jun 11, 9:16 pm, George Sakkis <george.sak...@gmail.com> wrote:
>> On Jun 11, 8:15 pm, bvdp <b...@mellowood.ca> wrote:
>>
>>
>>
>>> Matimus wrote:
>>>> The solution I posted should work and is safe. It may not seem very
>>>> readable, but it is using Pythons internal parser to parse the passed
>>>> in string into an abstract symbol tree (rather than code). Normally
>>>> Python would just use the ast internally to create code. Instead I've
>>>> written the code to do that. By avoiding anything but simple operators
>>>> and literals it is guaranteed safe.
>>> Just wondering ... how safe would:
>>> eval(s, {"__builtins__":None}, {} )
>>> be? From my testing it seems that it parses out numbers properly (int
>>> and float) and does simple math like +, -, **, etc. It doesn't do
>>> functions like int(), sin(), etc ... but that is fine for my puposes.
>>> Just playing a bit, it seems to give the same results as your code using
>>> ast does. I may be missing something!

>> Probably you do; within a couple of minutes I came up with this:
>>
>>>>> s = """

>> ... (t for t in 42 .__class__.__base__.__subclasses__()
>> ... if t.__name__ == 'file').next()('/etc/passwd')
>> ... """>>> eval(s, {"__builtins__":None}, {} )
>>
>> Traceback (most recent call last):
>> File "<stdin>", line 1, in <module>
>> File "<string>", line 3, in <module>
>> IOError: file() constructor not accessible in restricted mode
>>
>> Not an exploit yet but I wouldn't be surprised if there is one. Unless
>> you fully trust your users, an ast-based approach is your best bet.
>>
>> George

>
> You can get access to any new-style class that has been loaded. This
> exploit works on my machine (Windows XP).
>
>
Code:
> # This assumes that ctypes was loaded, but keep in mind any classes
> # that have been loaded are potentially accessible.
> 
> import ctypes
> 
> s = """
> (
>     t for t in 42 .__class__.__base__.__subclasses__()
>         if t.__name__ == 'LibraryLoader'
>     ).next()(
>         (
>             t for t in 42 .__class__.__base__.__subclasses__()
>                 if t.__name__ == 'CDLL'
>             ).next()
>         ).msvcrt.system('dir') # replace 'dir' with something nasty
> """
> 
> eval(s, {"__builtins__":None}, {})
>
>
> Matt


Yes, this is probably a good point. But, I don't see this as an exploit
in my program. Again, I could be wrong ... certainly not the first time
that has happened

In my case, the only way a user can use eval() is via my own parsing
which restricts this to a limited usage. So, the code setting up the
eval() exploit has to be entered via the "safe" eval to start with. So,
IF the code you present can be installed from within my program's
scripts ... then yes there can be a problem. But for the life of me I
don't see how this is possible. In my program we're just looking at
single lines in a script and doing commands based on the text.
Setting/evaluating macros is one "command" and I just want a method to
do something like "Set X 25 * 2" and passing the "25 * 2" string to
python works. If the user creates a script with "Set X os.system('rm
*')" and I used a clean eval() then we could have a meltdown ... but if
we stick with the eval(s, {"__builtins__":None}, {}) I don't see how the
malicious script could do the class modifications you suggest.

I suppose that someone could modify my program code and then cause my
eval() to fail (be unsafe). But, if we count on program modifications to
be doorways to exploits then we might as well just pull the plug.

Bob.
 
Reply With Quote
 
George Sakkis
Guest
Posts: n/a
 
      06-12-2008
On Jun 12, 1:51 pm, bvdp <b...@mellowood.ca> wrote:

> Matimus wrote:
> > On Jun 11, 9:16 pm, George Sakkis <george.sak...@gmail.com> wrote:
> >> On Jun 11, 8:15 pm, bvdp <b...@mellowood.ca> wrote:

>
> >>> Matimus wrote:
> >>>> The solution I posted should work and is safe. It may not seem very
> >>>> readable, but it is using Pythons internal parser to parse the passed
> >>>> in string into an abstract symbol tree (rather than code). Normally
> >>>> Python would just use the ast internally to create code. Instead I've
> >>>> written the code to do that. By avoiding anything but simple operators
> >>>> and literals it is guaranteed safe.
> >>> Just wondering ... how safe would:
> >>> eval(s, {"__builtins__":None}, {} )
> >>> be? From my testing it seems that it parses out numbers properly (int
> >>> and float) and does simple math like +, -, **, etc. It doesn't do
> >>> functions like int(), sin(), etc ... but that is fine for my puposes.
> >>> Just playing a bit, it seems to give the same results as your code using
> >>> ast does. I may be missing something!
> >> Probably you do; within a couple of minutes I came up with this:

>
> >>>>> s = """
> >> ... (t for t in 42 .__class__.__base__.__subclasses__()
> >> ... if t.__name__ == 'file').next()('/etc/passwd')
> >> ... """>>> eval(s, {"__builtins__":None}, {} )

>
> >> Traceback (most recent call last):
> >> File "<stdin>", line 1, in <module>
> >> File "<string>", line 3, in <module>
> >> IOError: file() constructor not accessible in restricted mode

>
> >> Not an exploit yet but I wouldn't be surprised if there is one. Unless
> >> you fully trust your users, an ast-based approach is your best bet.

>
> >> George

>
> > You can get access to any new-style class that has been loaded. This
> > exploit works on my machine (Windows XP).

>
> >
Code:
> > # This assumes that ctypes was loaded, but keep in mind any classes
> > # that have been loaded are potentially accessible.
Code:
>
> > import ctypes
>
> > s = """
> > (
> >     t for t in 42 .__class__.__base__.__subclasses__()
> >         if t.__name__ == 'LibraryLoader'
> >     ).next()(
> >         (
> >             t for t in 42 .__class__.__base__.__subclasses__()
> >                 if t.__name__ == 'CDLL'
> >             ).next()
> >         ).msvcrt.system('dir') # replace 'dir' with something nasty
> > """
>
> > eval(s, {"__builtins__":None}, {})
> > 

>
> > Matt

>
> Yes, this is probably a good point. But, I don't see this as an exploit
> in my program. Again, I could be wrong ... certainly not the first time
> that has happened
>
> In my case, the only way a user can use eval() is via my own parsing
> which restricts this to a limited usage. So, the code setting up the
> eval() exploit has to be entered via the "safe" eval to start with. So,
> IF the code you present can be installed from within my program's
> scripts ... then yes there can be a problem. But for the life of me I
> don't see how this is possible. In my program we're just looking at
> single lines in a script and doing commands based on the text.
> Setting/evaluating macros is one "command" and I just want a method to
> do something like "Set X 25 * 2" and passing the "25 * 2" string to
> python works. If the user creates a script with "Set X os.system('rm
> *')" and I used a clean eval() then we could have a meltdown ... but if
> we stick with the eval(s, {"__builtins__":None}, {}) I don't see how the
> malicious script could do the class modifications you suggest.
>
> I suppose that someone could modify my program code and then cause my
> eval() to fail (be unsafe). But, if we count on program modifications to
> be doorways to exploits then we might as well just pull the plug.


You probably missed the point in the posted examples. A malicious user
doesn't need to modify your program code to have access to far more
than you would hope, just devise an appropriate string s and pass it
to your "safe" eval.

Here's a simpler example to help you see the back doors that open. So
you might think that eval(s, {"__builtins__":None}, {}) doesn't
provide access to the `file` type. At first it looks so:

>>> eval('file', {"__builtins__":None}, {})

NameError: name 'file' is not defined
>>> eval('open', {"__builtins__":None}, {})

NameError: name 'open' is not defined

"Ok, I am safe from users messing with files since they can't even
access the file type" you reassure yourself. Then someone comes in and
passes to your "safe" eval this string:
>>> s = "(t for t in (42).__class__.__base__.__subclasses__() if t.__name__ == 'file').next()"
>>> eval(s, {"__builtins__":None}, {})

<type 'file'>

Oops.

Fortunately the file() constructor has apparently some extra logic
that prevents it from being used in restricted mode, but that doesn't
change the fact that file *is* available in restricted mode; you just
can't spell it "file".

25 builtin types are currently available in restricted mode -- without
any explicit import -- and this number has been increasing over the
years:

$ python2.3 -c 'print
eval("(42).__class__.__base__.__subclasses__().__l en__()",
{"__builtins__":None}, {})'
13

$ python2.4 -c 'print
eval("(42).__class__.__base__.__subclasses__().__l en__()",
{"__builtins__":None}, {})'
17

$ python2.5 -c 'print
eval("(42).__class__.__base__.__subclasses__().__l en__()",
{"__builtins__":None}, {})'
25

$ python2.6a1 -c 'print
eval("(42).__class__.__base__.__subclasses__().__l en__()",
{"__builtins__":None}, {})'
32

Regards,
George
 
Reply With Quote
 
bvdp
Guest
Posts: n/a
 
      06-12-2008
George Sakkis wrote:

> You probably missed the point in the posted examples. A malicious user
> doesn't need to modify your program code to have access to far more
> than you would hope, just devise an appropriate string s and pass it
> to your "safe" eval.


Oppps, I did miss the point. I was assuming that the modifying stuff was
being done before the call to the eval(). I was wrong.

I'll have to get the ast based code incorporated into my code and just
use it. Darn, but it seems that each and every time one sees a simple
solution to a simple problem ...

Thanks.

 
Reply With Quote
 
 
 
Reply

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off
Trackbacks are On
Pingbacks are On
Refbacks are Off


Similar Threads
Thread Thread Starter Forum Replies Last Post
Re: Simple and safe evaluator George Sakkis Python 6 06-20-2008 05:00 PM
Simple and safe evaluator bvdp Python 6 06-12-2008 11:50 AM
JavaScript interpreter/evaluator class? None Java 1 08-31-2004 05:27 AM
Any XPath Evaluator out there based on SAX??? Hyden Zeng XML 0 12-08-2003 08:21 AM
boolean expression evaluator - source code? Anonymous Java 1 11-24-2003 12:50 AM



Advertisments
 



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57