Restrict Genshi evaluation

Closes #14869 #5160
This commit is contained in:
Cédric Krier
2026-06-08 23:44:10 +02:00
parent 32cae6b2cb
commit d1aba4dbb5
4 changed files with 148 additions and 0 deletions
+1
View File
@@ -1,3 +1,4 @@
* Restrict Genshi evaluation (issue14869, issue5160)
* Add BrowseList for Model instances
* Remove cursor_dict
* Add row factory to cursor
+4
View File
@@ -12,6 +12,8 @@ from email import charset
import __main__
from lxml import etree, objectify
from ._safe_genshi import genshi_patch
__version__ = "8.1.0"
__series__ = '.'.join(__version__.split('.')[:2])
@@ -36,6 +38,8 @@ objectify.set_default_parser(objectify.makeparser(resolve_entities=False))
decimal.DefaultContext.prec = int(os.environ.get('TRYTOND_DECIMAL_PREC', 28))
genshi_patch()
class _RequestPatchFinder:
def find_spec(self, fullname, path, target=None):
+107
View File
@@ -0,0 +1,107 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from genshi.template.astutil import ASTTransformer
from genshi.template.eval import (
BUILTINS, Code, ExpressionASTTransformer, TemplateASTTransformer)
class _SafeASTTransformer(ASTTransformer):
def visit_Attribute(self, node):
if (node.attr.startswith('_')
and node.attr not in {'__class__', '__name__'}):
raise ValueError(f"invalid attribute {node.attr!r}")
return super().visit_Attribute(node)
def visit_Import(self, node):
raise ValueError("invalid import")
def visit_ImportFrom(self, node):
raise ValueError("invalid import from")
def visit_Name(self, node):
if (node.id.startswith('_')
and not node.id.startswith('__relatorio_')
and node.id not in {'_'}):
raise ValueError(f"invalid name {node.id!r}")
return super().visit_Name(node)
class SafeExpressionASTTransformer(
_SafeASTTransformer, ExpressionASTTransformer):
pass
class SafeTemplateASTTransformer(_SafeASTTransformer, TemplateASTTransformer):
pass
ALLOWED_BUILTINS = {
'False',
'True',
'None',
'abs',
'all',
'any',
'ascii',
'bin',
'bool',
'bytearray',
'bytes',
'chr',
'complex',
'dict',
'dir',
'divmod',
'enumerate',
'filter',
'float',
'format',
'frozenset',
'hasattr',
'hash',
'hex',
'int',
'iter',
'len',
'list',
'map',
'max',
'min',
'next',
'oct',
'ord',
'pow',
'range',
'repr',
'reversed',
'round',
'set'
'slice',
'sorted',
'str',
'sum',
'tuple',
'zip',
}
def genshi_patch():
code__init__ = Code.__init__
def patched_code__init__(
self, source, filename=None, lineno=-1, lookup='strict',
xform=None):
if self.mode == 'eval':
xform = SafeExpressionASTTransformer
else:
xform = SafeTemplateASTTransformer
code__init__(
self, source, filename=filename, lineno=lineno, lookup=lookup,
xform=xform)
Code.__init__ = patched_code__init__
for name in BUILTINS.keys() - ALLOWED_BUILTINS:
BUILTINS.pop(name)
+36
View File
@@ -0,0 +1,36 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from genshi.template import MarkupTemplate, TextTemplate
from genshi.template.eval import UndefinedError
from trytond.tests.test_tryton import TestCase
class GenshiTestCase(TestCase):
def test_no_builtins(self):
"Test no builtins"
with self.assertRaises(UndefinedError):
str(TextTemplate("${open('%s').read()}" % __file__).generate())
def test_no_private_name(self):
"Test no private name"
with self.assertRaisesRegex(ValueError, r"invalid name '__import__'"):
str(TextTemplate("${__import__('os')}").generate())
def test_no_private_attribute(self):
"Test no private attribute"
with self.assertRaisesRegex(
ValueError, r"invalid attribute '__getattribute__'"):
str(TextTemplate("${True.__getattribute__}").generate())
def test_no_import(self):
"Test no import"
with self.assertRaisesRegex(ValueError, r"invalid import"):
str(MarkupTemplate("<?python import os?>").generate())
def test_no_import_from(self):
"Test no import from"
with self.assertRaisesRegex(ValueError, r"invalid import from"):
str(MarkupTemplate("<?python from os import popen?>").generate())