@@ -1,3 +1,4 @@
|
||||
* Restrict Genshi evaluation (issue14869, issue5160)
|
||||
* Add BrowseList for Model instances
|
||||
* Remove cursor_dict
|
||||
* Add row factory to cursor
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user