Restrict Genshi evaluation
Closes #14869 #5160 (grafted from 101aaf12c6bb1a333c9105b1e6677a4a5f49605c) --HG-- branch : 7.0
This commit is contained in:
1 parent
ec42f804a6
commit
993cf9f948
4 files changed
+148
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Restrict Genshi evaluation (issue14869, issue5160)
|
||||
|
||||
Version 7.0.52 - 2026-06-18
|
||||
---------------------------
|
||||
|
||||
@@ -10,6 +10,8 @@ from email import charset
|
||||
import __main__
|
||||
from lxml import etree, objectify
|
||||
|
||||
from ._safe_genshi import genshi_patch
|
||||
|
||||
__version__ = "7.0.53"
|
||||
|
||||
if not os.environ.get('TRYTOND_APPNAME'):
|
||||
@@ -31,6 +33,8 @@ charset.add_charset('utf-8', charset.QP, charset.QP)
|
||||
etree.set_default_parser(etree.XMLParser(resolve_entities=False))
|
||||
objectify.set_default_parser(objectify.makeparser(resolve_entities=False))
|
||||
|
||||
genshi_patch()
|
||||
|
||||
|
||||
class _RequestPatchFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
import unittest
|
||||
|
||||
from genshi.template import MarkupTemplate, TextTemplate
|
||||
from genshi.template.eval import UndefinedError
|
||||
|
||||
|
||||
class GenshiTestCase(unittest.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())
|
||||
Reference in new issue
Block a user