Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Changes
- Drop support of PyPy as there currently seems to be no way to restrict the
builtins. See https://bitbucket.org/pypy/pypy/issues/2653.

- Security issue: No longer allow using the ``format`` method of str resp.
unicode. See http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/.


4.0a3 (2017-06-20)
------------------
Expand Down
8 changes: 7 additions & 1 deletion src/RestrictedPython/Eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
##############################################################################
"""Restricted Python Expressions."""

from . import Guards
from ._compat import IS_PY2
from .compile import compile_restricted_eval

Expand All @@ -38,7 +39,12 @@ def default_guarded_getitem(ob, index):
class RestrictionCapableEval(object):
"""A base class for restricted code."""

globals = {'__builtins__': None}
globals = {
'__builtins__': None,
'_str_': Guards.SecureStr,
}
if IS_PY2:
globals['_unicode_'] = Guards.SecureUnicode
# restricted
rcode = None

Expand Down
26 changes: 23 additions & 3 deletions src/RestrictedPython/Guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
'repr',
'round',
'slice',
'str',
'tuple',
'zip'
]
Expand Down Expand Up @@ -109,11 +108,10 @@

if IS_PY2:
_safe_names.extend([
'basestring',
'basestring', # cannot be instantiated
'cmp',
'long',
'unichr',
'unicode',
'xrange',
])
_safe_exceptions.extend([
Expand Down Expand Up @@ -189,6 +187,28 @@
# type


class SecureStr(str):
"""Str class which does not allow unsafe methods."""

def format(*args, **kw):
raise NotImplementedError('Using format() is not safe.')


safe_builtins['str'] = SecureStr
safe_builtins['_str_'] = SecureStr


if IS_PY2:
class SecureUnicode(unicode):
"""Unicode class which does not allow unsafe methods."""

def format(*args, **kw):
raise NotImplementedError('Using format() is not safe.')

safe_builtins['unicode'] = SecureUnicode
safe_builtins['_unicode_'] = SecureUnicode


def _write_wrapper():
# Construct the write wrapper class
def _handler(secattr, error_msg):
Expand Down
19 changes: 13 additions & 6 deletions src/RestrictedPython/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ controlled and restricted execution of code:
... def hello_world():
... return "Hello World!"
... '''
>>> from RestrictedPython import compile_restricted
>>> from RestrictedPython import compile_restricted, safe_builtins
>>> code = compile_restricted(src, '<string>', 'exec')

The resulting code can be executed using the ``exec`` built-in:

>>> exec(code)
>>> glob = {'__builtins__': safe_builtins}
>>> exec(code, glob)

As a result, the ``hello_world`` function is now available in the
global namespace:
As a result, the ``hello_world`` function is now available in the ``glob``
dict:

>>> hello_world()
>>> glob['hello_world']()
'Hello World!'

Compatibility
Expand Down Expand Up @@ -60,7 +61,12 @@ Specifically:
4. ``__import__`` is the normal Python import hook, and should be used
to control access to Python packages and modules.

5. ``__builtins__`` is the normal Python builtins dictionary, which
5. ``_str_`` and ``_unicode_`` (the latter is Python 2 only) are the factories
to create string resp. unicode objects. If using ``safe_builtins``
(see next item) they are predefined with secured subclasses of ``str`` resp.
``unicode``.

6. ``__builtins__`` is the normal Python builtins dictionary, which
should be weeded down to a set that cannot be used to get around
your restrictions. A usable "safe" set is
``RestrictedPython.Guards.safe_builtins``.
Expand Down Expand Up @@ -100,6 +106,7 @@ callable, from which the restricted machinery will create the object):
>>> from RestrictedPython.PrintCollector import PrintCollector
>>> _print_ = PrintCollector
>>> _getattr_ = getattr
>>> _str_ = str

>>> src = '''
... print("Hello World!")
Expand Down
11 changes: 10 additions & 1 deletion src/RestrictedPython/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,16 @@ def visit_Num(self, node):

def visit_Str(self, node):
"""Allow string literals without restrictions."""
return self.node_contents_visit(node)
node = self.node_contents_visit(node)
factory_name = '_str_'
if IS_PY2 and isinstance(node.s, unicode):
factory_name = '_unicode_'
new_node = ast.Call(
func=ast.Name(factory_name, ast.Load()),
args=[node],
keywords=[])
copy_locations(new_node, node)
return new_node

def visit_Bytes(self, node):
"""Allow bytes literals without restrictions.
Expand Down
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def _eval(source, glb=None):
if glb is None:
glb = {}
return eval(code, glb)
# The next line can be dropped after the old implementation was dropped.
_eval.compile_func = compile_func
return _eval


Expand Down
2 changes: 2 additions & 0 deletions tests/test_compile_restricted_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def test_compile_restricted_function_pretends_the_code_is_executed_in_a_global_s
safe_globals = {
'__name__': 'script',
'output': 'foo',
'_str_': str,
}
# safe_globals.update(safe_builtins)
safe_locals = {}
Expand Down Expand Up @@ -195,6 +196,7 @@ def test_compile_restricted_function_allows_invalid_python_identifiers_as_functi
safe_globals = {
'__name__': 'script',
'output': 'foo',
'_str_': str,
}
# safe_globals.update(safe_builtins)
safe_locals = {}
Expand Down
21 changes: 16 additions & 5 deletions tests/test_print_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@


def test_print_function__simple_prints():
glb = {'_print_': PrintCollector, '_getattr_': None}
glb = {
'_print_': PrintCollector,
'_getattr_': None,
'_str_': str,
}

code, errors = compiler(ALLOWED_PRINT_FUNCTION)[:2]
assert errors == ()
Expand Down Expand Up @@ -131,7 +135,8 @@ def test_print_function_with_kw_args(mocker):
glb = {
'_print_': PrintCollector,
'_getattr_': None,
"_apply_": _apply_
"_apply_": _apply_,
'_str_': str,
}

code, errors = compiler(ALLOWED_PRINT_FUNCTION_WITH_KWARGS)[:2]
Expand Down Expand Up @@ -164,7 +169,8 @@ def test_print_function__protect_file(mocker):
glb = {
'_print_': PrintCollector,
'_getattr_': _getattr_,
'stream': stream
'stream': stream,
'_str_': str,
}

code, errors = compiler(PROTECT_WRITE_ON_FILE)[:2]
Expand Down Expand Up @@ -207,7 +213,11 @@ def main():
def test_print_function__nested_print_collector():
code, errors = compiler(INJECT_PRINT_COLLECTOR_NESTED)[:2]

glb = {"_print_": PrintCollector, '_getattr_': None}
glb = {
'_print_': PrintCollector,
'_getattr_': None,
'_str_': str,
}
exec(code, glb)

ret = glb['main']()
Expand Down Expand Up @@ -307,7 +317,8 @@ def test_print_function_no_new_scope():
'_print_': PrintCollector,
'__metaclass__': type,
'_getattr_': None,
'_getiter_': lambda ob: ob
'_getiter_': lambda ob: ob,
'_str_': str,
}
exec(code, glb)

Expand Down
30 changes: 25 additions & 5 deletions tests/test_print_stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@

@pytest.mark.parametrize(*c_exec)
def test_print_stmt__simple_prints(c_exec):
glb = {'_print_': PrintCollector, '_getattr_': None}
glb = {
'_print_': PrintCollector,
'_getattr_': None,
'_str_': str,
}

code, errors = c_exec(ALLOWED_PRINT_STATEMENT)[:2]
assert code is not None
Expand Down Expand Up @@ -96,7 +100,11 @@ def test_print_stmt__protect_chevron_print(c_exec, mocker):

_getattr_ = mocker.stub()
_getattr_.side_effect = getattr
glb = {'_getattr_': _getattr_, '_print_': PrintCollector}
glb = {
'_getattr_': _getattr_,
'_print_': PrintCollector,
'_str_': str,
}

exec(code, glb)

Expand Down Expand Up @@ -137,7 +145,11 @@ def main():
def test_print_stmt__nested_print_collector(c_exec, mocker):
code, errors = c_exec(INJECT_PRINT_COLLECTOR_NESTED)[:2]

glb = {"_print_": PrintCollector, '_getattr_': None}
glb = {
'_print_': PrintCollector,
'_getattr_': None,
'_str_': str,
}
exec(code, glb)

ret = glb['main']()
Expand Down Expand Up @@ -255,7 +267,11 @@ class A:
@pytest.mark.parametrize(*c_exec)
def test_print_stmt_no_new_scope(c_exec):
code, errors = c_exec(NO_PRINT_SCOPES)[:2]
glb = {'_print_': PrintCollector, '_getattr_': None}
glb = {
'_print_': PrintCollector,
'_getattr_': None,
'_str_': str,
}
exec(code, glb)

ret = glb['class_scope']()
Expand All @@ -273,7 +289,11 @@ def func(cond):
@pytest.mark.parametrize(*c_exec)
def test_print_stmt_conditional_print(c_exec):
code, errors = c_exec(CONDITIONAL_PRINT)[:2]
glb = {'_print_': PrintCollector, '_getattr_': None}
glb = {
'_print_': PrintCollector,
'_getattr_': None,
'_str_': str,
}
exec(code, glb)

assert glb['func'](True) == '1\n'
Expand Down
1 change: 1 addition & 0 deletions tests/transformer/test_attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def test_RestrictingNodeTransformer__visit_Attribute__5(
"""It transforms writing to an attribute to `_write_`."""
glb = {
'_write_': mocker.stub(),
'_str_': str,
'a': mocker.stub(),
}
glb['_write_'].return_value = glb['a']
Expand Down
79 changes: 78 additions & 1 deletion tests/transformer/test_base_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from RestrictedPython import safe_builtins
from RestrictedPython._compat import IS_PY2
from RestrictedPython._compat import IS_PY3
from tests import c_exec
from tests import e_eval
from tests import e_exec

import pytest

Expand All @@ -14,7 +17,8 @@ def test_Num(e_eval):
@pytest.mark.parametrize(*e_eval)
def test_Bytes(e_eval):
"""It allows to use bytes literals."""
assert e_eval('b"code"') == b"code"
glb = {'_str_': str}
assert e_eval('b"code"', glb) == b"code"


@pytest.mark.parametrize(*e_eval)
Expand All @@ -30,3 +34,76 @@ def test_Ellipsis(c_exec):
"""It prevents using the `ellipsis` statement."""
result = c_exec('...')
assert result.errors == ('Line 1: Ellipsis statements are not allowed.',)


@pytest.mark.parametrize(*e_exec)
def test_Str__1(e_exec):
"""It returns a str subclass for strings."""
glb = {
'_getattr_': getattr,
'__builtins__': safe_builtins,
}
e_exec('a = "Hello world!"', glb)
assert isinstance(glb['a'], str)


@pytest.mark.skipif(IS_PY3, reason="In Python 3 there is no unicode.")
@pytest.mark.parametrize(*e_exec)
def test_Str__2(e_exec):
"""It returns a unicode subclass for unicodes."""
glb = {
'_getattr_': getattr,
'__builtins__': safe_builtins,
}
e_exec('a = u"Hello world!"', glb)
assert isinstance(glb['a'], unicode)


STRING_DOT_FORMAT_DENIED = """\
'Hello {}'.format('world')
"""


@pytest.mark.parametrize(*e_eval)
def test_Str__3(e_eval):
"""It prevents using the format method of a string.

format() is considered harmful:
http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
"""
if IS_PY2:
from RestrictedPython import RCompile
if e_eval.compile_func is RCompile.compile_restricted_eval:
pytest.skip('RCompile does not support secure strings.')
glb = {
'_getattr_': getattr,
'__builtins__': safe_builtins,
}
with pytest.raises(NotImplementedError) as err:
e_eval(STRING_DOT_FORMAT_DENIED, glb)
assert 'Using format() is not safe.' == str(err.value)


UNICODE_DOT_FORMAT_DENIED = """\
u'Hello {}'.format('world')
"""


@pytest.mark.parametrize(*e_eval)
def test_Str__4(e_eval):
"""It prevents using the format method of a unicode.

format() is considered harmful:
http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
"""
if IS_PY2:
from RestrictedPython import RCompile
if e_eval.compile_func is RCompile.compile_restricted_eval:
pytest.skip('RCompile does not support secure unicode.')
glb = {
'_getattr_': getattr,
'__builtins__': safe_builtins,
}
with pytest.raises(NotImplementedError) as err:
e_eval(UNICODE_DOT_FORMAT_DENIED, glb)
assert 'Using format() is not safe.' == str(err.value)
Loading