details: https://code.tryton.org/tryton/commit/e033d154c7c0
branch: 7.8
user: Cédric Krier <[email protected]>
date: Mon Jun 08 23:44:10 2026 +0200
description:
Restrict Genshi evaluation
Closes #14869 #5160
(grafted from 101aaf12c6bb1a333c9105b1e6677a4a5f49605c)
diffstat:
trytond/CHANGELOG | 1 +
trytond/trytond/__init__.py | 4 +
trytond/trytond/_safe_genshi.py | 107 +++++++++++++++++++++++++++++++++++
trytond/trytond/tests/test_genshi.py | 36 +++++++++++
4 files changed, 148 insertions(+), 0 deletions(-)
diffs (180 lines):
diff -r 9fc345b4ebe8 -r e033d154c7c0 trytond/CHANGELOG
--- a/trytond/CHANGELOG Mon Jun 29 18:06:10 2026 +0200
+++ b/trytond/CHANGELOG Mon Jun 08 23:44:10 2026 +0200
@@ -1,3 +1,4 @@
+* Restrict Genshi evaluation (issue14869, issue5160)
Version 7.8.11 - 2026-06-18
---------------------------
diff -r 9fc345b4ebe8 -r e033d154c7c0 trytond/trytond/__init__.py
--- a/trytond/trytond/__init__.py Mon Jun 29 18:06:10 2026 +0200
+++ b/trytond/trytond/__init__.py Mon Jun 08 23:44:10 2026 +0200
@@ -12,6 +12,8 @@
import __main__
from lxml import etree, objectify
+from ._safe_genshi import genshi_patch
+
__version__ = "7.8.12"
__series__ = '.'.join(__version__.split('.')[:2])
@@ -36,6 +38,8 @@
decimal.DefaultContext.prec = int(os.environ.get('TRYTOND_DECIMAL_PREC', 28))
+genshi_patch()
+
class _RequestPatchFinder:
def find_spec(self, fullname, path, target=None):
diff -r 9fc345b4ebe8 -r e033d154c7c0 trytond/trytond/_safe_genshi.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/trytond/_safe_genshi.py Mon Jun 08 23:44:10 2026 +0200
@@ -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)
diff -r 9fc345b4ebe8 -r e033d154c7c0 trytond/trytond/tests/test_genshi.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/trytond/tests/test_genshi.py Mon Jun 08 23:44:10 2026 +0200
@@ -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())