Skip to content

Commit 3b47440

Browse files
icemacMichael Howitzdataflake
authored
Merge commit from fork
* Block unsafe string.Formatter access * Add additional cases from kakashi-1337 --------- Co-authored-by: Michael Howitz <mh@gocept.com> Co-authored-by: Jens Vagelpohl <jens@plyp.com>
1 parent bd256e6 commit 3b47440

3 files changed

Lines changed: 147 additions & 0 deletions

File tree

‎CHANGES.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Changes
1111

1212
- Disallow ``mode="function"`` in ``compile_restricted`` (it never worked).
1313

14+
- Prevent access to ``string.Formatter`` and its unsafe traversal methods via
15+
``safer_getattr``.
16+
1417
8.3 (2026-06-16)
1518
----------------
1619

‎src/RestrictedPython/Guards.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# DocumentTemplate.DT_UTil contains a few.
1717

1818
import builtins
19+
import string
1920

2021
from RestrictedPython.transformer import INSPECT_ATTRIBUTES
2122

@@ -238,6 +239,8 @@ def guarded_delattr(object, name):
238239

239240

240241
raise_ = object()
242+
_FORMATTER_UNSAFE_METHODS = frozenset(('format', 'get_field', 'get_value',
243+
'vformat'))
241244

242245

243246
def safer_getattr(object, name, default=None, getattr=getattr):
@@ -254,6 +257,14 @@ def safer_getattr(object, name, default=None, getattr=getattr):
254257
(isinstance(object, type) and issubclass(object, str))):
255258
raise NotImplementedError(
256259
'Using the format*() methods of `str` is not safe')
260+
if object is string and name == 'Formatter':
261+
raise NotImplementedError('string.Formatter is not safe')
262+
if name in _FORMATTER_UNSAFE_METHODS and (
263+
isinstance(object, string.Formatter) or
264+
(isinstance(object, type) and
265+
issubclass(object, string.Formatter))):
266+
raise NotImplementedError(
267+
'Using string.Formatter methods is not safe')
257268
if name in INSPECT_ATTRIBUTES:
258269
raise AttributeError(
259270
f'"{name}" is a restricted name,'

‎tests/test_Guards.py‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import string as pystring
2+
13
import pytest
24

35
from RestrictedPython import compile_restricted_exec
@@ -234,6 +236,137 @@ def test_Guards__safer_getattr__1d():
234236
assert 'Using the format*() methods of `str` is not safe' == str(err.value)
235237

236238

239+
STRING_FORMATTER_GET_FIELD_DENIED = """\
240+
import string
241+
fmt = string.Formatter()
242+
# Build restricted attribute names without spelling underscores directly.
243+
U = chr(95)
244+
g = U*2 + 'globals' + U*2
245+
b = U*2 + 'builtins' + U*2
246+
src = string.capwords
247+
real_builtins = fmt.get_field('0.' + g + '[' + b + ']', (src,), {})[0]
248+
result = real_builtins['eval']('1+1')
249+
"""
250+
251+
252+
def test_Guards__safer_getattr__1e():
253+
"""It prevents access to the real ``string.Formatter`` class."""
254+
builtins = safe_builtins.copy()
255+
builtins['__import__'] = __import__
256+
glb = {
257+
'__builtins__': builtins,
258+
}
259+
with pytest.raises(NotImplementedError) as err:
260+
restricted_exec(STRING_FORMATTER_GET_FIELD_DENIED, glb)
261+
assert 'string.Formatter is not safe' == str(err.value)
262+
263+
264+
def test_Guards__safer_getattr__1f():
265+
"""It prevents unsafe methods on provided ``Formatter`` instances."""
266+
fmt = pystring.Formatter()
267+
268+
for name in ('format', 'get_field', 'get_value', 'vformat'):
269+
with pytest.raises(NotImplementedError) as err:
270+
safer_getattr(fmt, name)
271+
assert 'Using string.Formatter methods is not safe' == str(err.value)
272+
273+
274+
DIRECT_STRING_FORMATTER_CLASS_GET_FIELD_DENIED = """\
275+
fmt = Formatter()
276+
result = fmt.get_field('0', (capwords,), {})
277+
"""
278+
279+
280+
DIRECT_STRING_FORMATTER_INSTANCE_GET_FIELD_DENIED = """\
281+
result = fmt.get_field('0', (capwords,), {})
282+
"""
283+
284+
285+
def test_Guards__safer_getattr__1g():
286+
"""It prevents traversal if the host provides ``Formatter`` directly."""
287+
glb = {
288+
'__builtins__': safe_builtins,
289+
'Formatter': pystring.Formatter,
290+
'capwords': pystring.capwords,
291+
}
292+
with pytest.raises(NotImplementedError) as err:
293+
restricted_exec(DIRECT_STRING_FORMATTER_CLASS_GET_FIELD_DENIED, glb)
294+
assert 'Using string.Formatter methods is not safe' == str(err.value)
295+
296+
297+
def test_Guards__safer_getattr__1h():
298+
"""It prevents traversal if the host provides a ``Formatter`` instance."""
299+
glb = {
300+
'__builtins__': safe_builtins,
301+
'fmt': pystring.Formatter(),
302+
'capwords': pystring.capwords,
303+
}
304+
with pytest.raises(NotImplementedError) as err:
305+
restricted_exec(DIRECT_STRING_FORMATTER_INSTANCE_GET_FIELD_DENIED, glb)
306+
assert 'Using string.Formatter methods is not safe' == str(err.value)
307+
308+
309+
UNBOUND_STRING_FORMATTER_CLASS_GET_FIELD_DENIED = """\
310+
fmt = Formatter()
311+
gf = Formatter.get_field
312+
result = gf(fmt, '0', (capwords,), {})
313+
"""
314+
315+
316+
def test_Guards__safer_getattr__1i():
317+
"""It prevents unbound (class-level) access to ``Formatter.get_field``."""
318+
glb = {
319+
'__builtins__': safe_builtins,
320+
'Formatter': pystring.Formatter,
321+
'capwords': pystring.capwords,
322+
}
323+
with pytest.raises(NotImplementedError) as err:
324+
restricted_exec(UNBOUND_STRING_FORMATTER_CLASS_GET_FIELD_DENIED, glb)
325+
assert 'Using string.Formatter methods is not safe' == str(err.value)
326+
327+
328+
UNBOUND_STRING_FORMATTER_CLASS_VFORMAT_DENIED = """\
329+
fmt = Formatter()
330+
vf = Formatter.vformat
331+
result = vf(fmt, '{0}', (capwords,), {})
332+
"""
333+
334+
335+
def test_Guards__safer_getattr__1j():
336+
"""It prevents unbound (class-level) access to ``Formatter.vformat``."""
337+
glb = {
338+
'__builtins__': safe_builtins,
339+
'Formatter': pystring.Formatter,
340+
'capwords': pystring.capwords,
341+
}
342+
with pytest.raises(NotImplementedError) as err:
343+
restricted_exec(UNBOUND_STRING_FORMATTER_CLASS_VFORMAT_DENIED, glb)
344+
assert 'Using string.Formatter methods is not safe' == str(err.value)
345+
346+
347+
class _ExposedFormatterSubclass(pystring.Formatter):
348+
"""A Formatter subclass a host might expose to restricted code."""
349+
350+
351+
UNBOUND_STRING_FORMATTER_SUBCLASS_GET_VALUE_DENIED = """\
352+
gv = Sub.get_value
353+
result = gv(Sub(), 0, (capwords,), {})
354+
"""
355+
356+
357+
def test_Guards__safer_getattr__1k():
358+
"""Prevents class-level access on host-exposed ``Formatter`` subclass."""
359+
glb = {
360+
'__builtins__': safe_builtins,
361+
'Sub': _ExposedFormatterSubclass,
362+
'capwords': pystring.capwords,
363+
}
364+
with pytest.raises(NotImplementedError) as err:
365+
restricted_exec(
366+
UNBOUND_STRING_FORMATTER_SUBCLASS_GET_VALUE_DENIED, glb)
367+
assert 'Using string.Formatter methods is not safe' == str(err.value)
368+
369+
237370
SAFER_GETATTR_ALLOWED = """\
238371
class A:
239372

0 commit comments

Comments
 (0)