Package: release.debian.org Severity: normal Tags: bookworm X-Debbugs-Cc: python-report...@packages.debian.org, secur...@debian.org Control: affects -1 + src:python-reportlab User: release.debian....@packages.debian.org Usertags: pu Control: tags -1 + security
[ Reason ] CVE-2023-33733 [ Impact ] RCE [ Tests ] Yes package test run at build time [ Risks ] Low [ Checklist ] [X] *all* changes are documented in the d/changelog [X] I reviewed all changes and I approve them [X] attach debdiff against the package in (old)stable [X] the issue is verified as fixed in unstable [ Changes ] - CVE-2023-33733 fix - salsa CI [ Other info ] Did you prefer a DSA upload or a PU
diff -Nru python-reportlab-3.6.12/debian/changelog python-reportlab-3.6.12/debian/changelog --- python-reportlab-3.6.12/debian/changelog 2022-12-31 10:05:33.000000000 +0000 +++ python-reportlab-3.6.12/debian/changelog 2024-10-12 17:14:35.000000000 +0000 @@ -1,3 +1,13 @@ +python-reportlab (3.6.12-1+deb12u1) bookworm-security; urgency=high + + * Team upload + * Fix CVE-2023-33733 + Reportlab was vulnerable to Remote Code Execution (RCE) + via crafted PDF file. + * Add SalsaCI + + -- Bastien Roucari??s <ro...@debian.org> Sat, 12 Oct 2024 17:14:35 +0000 + python-reportlab (3.6.12-1) unstable; urgency=medium * New upstream version. diff -Nru python-reportlab-3.6.12/debian/patches/0005-CVE-2023-33733-RCE-via-crafted-PDF-file.patch python-reportlab-3.6.12/debian/patches/0005-CVE-2023-33733-RCE-via-crafted-PDF-file.patch --- python-reportlab-3.6.12/debian/patches/0005-CVE-2023-33733-RCE-via-crafted-PDF-file.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-reportlab-3.6.12/debian/patches/0005-CVE-2023-33733-RCE-via-crafted-PDF-file.patch 2024-10-12 17:14:35.000000000 +0000 @@ -0,0 +1,344 @@ +From: Robin Becker <reportlab-us...@lists2.reportlab.com> +Date: Mon, 24 Apr 2023 13:52:40 +0100 +Subject: CVE-2023-33733 RCE via crafted PDF file + +origin: https://hg.reportlab.com/hg-public/reportlab/rev/1c39d2db15bb +bug: https://github.com/c53elyas/CVE-2023-33733 +--- + src/reportlab/lib/colors.py | 79 +++++++++++++++++++++++++++++++-------- + src/reportlab/lib/rl_safe_eval.py | 71 ++++++++++++++++++++++++++++++++++- + src/reportlab/lib/utils.py | 2 +- + src/reportlab/rl_settings.py | 4 +- + tests/test_lib_rl_safe_eval.py | 49 ++++++++++++++++++++++-- + 5 files changed, 181 insertions(+), 24 deletions(-) + +diff --git a/src/reportlab/lib/colors.py b/src/reportlab/lib/colors.py +index b7487db..839a674 100644 +--- a/src/reportlab/lib/colors.py ++++ b/src/reportlab/lib/colors.py +@@ -41,7 +41,8 @@ ValueError: css color 'pcmyka(100,0,0,0)' has wrong number of components + ''' + import math, re, functools + from reportlab.lib.rl_accel import fp_str +-from reportlab.lib.utils import asNative, isStr, rl_safe_eval ++from reportlab.lib.utils import asNative, isStr, rl_safe_eval, rl_extended_literal_eval ++from reportlab import rl_config + from ast import literal_eval + + class Color: +@@ -882,6 +883,17 @@ def parseColorClassFromString(arg): + return None + + class toColor: ++ """Accepot an expression returnng a Color subclass. ++ ++ This used to accept arbitrary Python expressions, which resulted in increasngly devilish CVEs and ++ security holes from tie to time. In April 2023 we are creating explicit, "dumb" parsing code to ++ replace this. Acceptable patterns are ++ ++ a Color instance passed in by the Python programmer ++ a named list of colours ('pink' etc') ++ list of 3 or 4 numbers ++ all CSS colour expression ++ """ + _G = {} #globals we like (eventually) + + def __init__(self): +@@ -907,22 +919,57 @@ class toColor: + C = getAllNamedColors() + s = arg.lower() + if s in C: return C[s] +- G = C.copy() +- G.update(self.extraColorsNS) +- if not self._G: ++ ++ ++ # allow expressions like 'Blacker(red, 0.5)' ++ # >>> re.compile(r"(Blacker|Whiter)\((\w+)\,\s?([0-9.]+)\)").match(msg).groups() ++ # ('Blacker', 'red', '0.5') ++ # >>> ++ pat = re.compile(r"(Blacker|Whiter)\((\w+)\,\s?([0-9.]+)\)") ++ m = pat.match(arg) ++ if m: ++ funcname, rootcolor, num = m.groups() ++ if funcname == 'Blacker': ++ return Blacker(rootcolor, float(num)) ++ else: ++ return Whiter(rootcolor, float(num)) ++ ++ try: ++ import ast ++ expr = ast.literal_eval(arg) #safe probably only a tuple or list of values ++ return toColor(expr) ++ except (SyntaxError, ValueError): ++ pass ++ ++ if rl_config.toColorCanUse=='rl_safe_eval': ++ #the most dangerous option ++ G = C.copy() ++ G.update(self.extraColorsNS) ++ if not self._G: ++ C = globals() ++ self._G = {s:C[s] for s in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter ++ _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK ++ _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance ++ cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb isStr linearlyInterpolatedColor ++ literal_eval obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split()} ++ G.update(self._G) ++ try: ++ return toColor(rl_safe_eval(arg,g=G,l={})) ++ except: ++ pass ++ elif rl_config.toColorCanUse=='rl_extended_literal_eval': + C = globals() +- self._G = {s:C[s] for s in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter +- _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK +- _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance +- cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb isStr linearlyInterpolatedColor +- literal_eval obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split()} +- G.update(self._G) +- #try: +- # return toColor(rl_safe_eval(arg,g=G,l={})) +- #except: +- # pass +- parsedColor = parseColorClassFromString(arg) +- if (parsedColor): return parsedColor ++ S = getAllNamedColors().copy() ++ C = {k:C[k] for k in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter ++ _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK ++ _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance ++ cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb linearlyInterpolatedColor ++ obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split() ++ if callable(C.get(k,None))} ++ try: ++ return rl_extended_literal_eval(arg,C,S) ++ except (ValueError, SyntaxError): ++ pass + + try: + return HexColor(arg) +diff --git a/src/reportlab/lib/rl_safe_eval.py b/src/reportlab/lib/rl_safe_eval.py +index 49828c9..50834f6 100644 +--- a/src/reportlab/lib/rl_safe_eval.py ++++ b/src/reportlab/lib/rl_safe_eval.py +@@ -3,7 +3,7 @@ + #https://github.com/zopefoundation/RestrictedPython + #https://github.com/danthedeckie/simpleeval + #hopefully we are standing on giants' shoulders +-import sys, os, ast, re, weakref, time, copy, math ++import sys, os, ast, re, weakref, time, copy, math, types + eval_debug = int(os.environ.get('EVAL_DEBUG','0')) + strTypes = (bytes,str) + isPy39 = sys.version_info[:2]>=(3,9) +@@ -53,7 +53,9 @@ __rl_unsafe__ = frozenset('''builtins breakpoint __annotations__ co_argcount co_ + func_doc func_globals func_name gi_code gi_frame gi_running gi_yieldfrom + __globals__ im_class im_func im_self __iter__ __kwdefaults__ __module__ + __name__ next __qualname__ __self__ tb_frame tb_lasti tb_lineno tb_next +- globals vars locals'''.split() ++ globals vars locals ++ type eval exec aiter anext compile open ++ dir print classmethod staticmethod __import__ super property'''.split() + ) + __rl_unsafe_re__ = re.compile(r'\b(?:%s)' % '|'.join(__rl_unsafe__),re.M) + +@@ -1204,5 +1206,70 @@ class __rl_safe_eval__: + class __rl_safe_exec__(__rl_safe_eval__): + mode = 'exec' + ++def rl_extended_literal_eval(expr, safe_callables=None, safe_names=None): ++ if safe_callables is None: ++ safe_callables = {} ++ if safe_names is None: ++ safe_names = {} ++ safe_names = safe_names.copy() ++ safe_names.update({'None': None, 'True': True, 'False': False}) ++ #make these readonly with MappingProxyType ++ safe_names = types.MappingProxyType(safe_names) ++ safe_callables = types.MappingProxyType(safe_callables) ++ if isinstance(expr, str): ++ expr = ast.parse(expr, mode='eval') ++ if isinstance(expr, ast.Expression): ++ expr = expr.body ++ try: ++ # Python 3.4 and up ++ ast.NameConstant ++ safe_test = lambda n: isinstance(n, ast.NameConstant) or isinstance(n,ast.Name) and n.id in safe_names ++ safe_extract = lambda n: n.value if isinstance(n,ast.NameConstant) else safe_names[n.id] ++ except AttributeError: ++ # Everything before ++ safe_test = lambda n: isinstance(n, ast.Name) and n.id in safe_names ++ safe_extract = lambda n: safe_names[n.id] ++ def _convert(node): ++ if isinstance(node, (ast.Str, ast.Bytes)): ++ return node.s ++ elif isinstance(node, ast.Num): ++ return node.n ++ elif isinstance(node, ast.Tuple): ++ return tuple(map(_convert, node.elts)) ++ elif isinstance(node, ast.List): ++ return list(map(_convert, node.elts)) ++ elif isinstance(node, ast.Dict): ++ return dict((_convert(k), _convert(v)) for k, v ++ in zip(node.keys, node.values)) ++ elif safe_test(node): ++ return safe_extract(node) ++ elif isinstance(node, ast.UnaryOp) and \ ++ isinstance(node.op, (ast.UAdd, ast.USub)) and \ ++ isinstance(node.operand, (ast.Num, ast.UnaryOp, ast.BinOp)): ++ operand = _convert(node.operand) ++ if isinstance(node.op, ast.UAdd): ++ return + operand ++ else: ++ return - operand ++ elif isinstance(node, ast.BinOp) and \ ++ isinstance(node.op, (ast.Add, ast.Sub)) and \ ++ isinstance(node.right, (ast.Num, ast.UnaryOp, ast.BinOp)) and \ ++ isinstance(node.right.n, complex) and \ ++ isinstance(node.left, (ast.Num, ast.UnaryOp, astBinOp)): ++ left = _convert(node.left) ++ right = _convert(node.right) ++ if isinstance(node.op, ast.Add): ++ return left + right ++ else: ++ return left - right ++ elif isinstance(node, ast.Call) and \ ++ isinstance(node.func, ast.Name) and \ ++ node.func.id in safe_callables: ++ return safe_callables[node.func.id]( ++ *[_convert(n) for n in node.args], ++ **{kw.arg: _convert(kw.value) for kw in node.keywords}) ++ raise ValueError('Bad expression') ++ return _convert(expr) ++ + rl_safe_exec = __rl_safe_exec__() + rl_safe_eval = __rl_safe_eval__() +diff --git a/src/reportlab/lib/utils.py b/src/reportlab/lib/utils.py +index 5a6b5d7..a53a05c 100644 +--- a/src/reportlab/lib/utils.py ++++ b/src/reportlab/lib/utils.py +@@ -11,7 +11,7 @@ from io import BytesIO + from hashlib import md5 + + from reportlab.lib.rltempfile import get_rl_tempfile, get_rl_tempdir +-from . rl_safe_eval import rl_safe_exec, rl_safe_eval, safer_globals ++from . rl_safe_eval import rl_safe_exec, rl_safe_eval, safer_globals, rl_extended_literal_eval + from PIL import Image + + class __UNSET__: +diff --git a/src/reportlab/rl_settings.py b/src/reportlab/rl_settings.py +index 0cf5150..a11289f 100644 +--- a/src/reportlab/rl_settings.py ++++ b/src/reportlab/rl_settings.py +@@ -69,7 +69,8 @@ trustedHosts + trustedSchemes + renderPMBackend + xmlParser +-textPaths'''.split()) ++textPaths ++toColorCanUse'''.split()) + + allowTableBoundsErrors = 1 # set to 0 to die on too large elements in tables in debug (recommend 1 for production use) + shapeChecking = 1 +@@ -163,6 +164,7 @@ xmlParser='lxml' #or 'pyrxp' for preferred xm + textPaths='backend' #freetype or _renderPM or backend + #determines what code is used to create Paths from str + #see reportlab/graphics/utils.py for full horror ++toColorCanUse='rl_extended_literal_eval' #change to None or 'rl_safe_eval' depending on trust + + # places to look for T1Font information + T1SearchPath = ( +diff --git a/tests/test_lib_rl_safe_eval.py b/tests/test_lib_rl_safe_eval.py +index 84bd86f..fd556eb 100644 +--- a/tests/test_lib_rl_safe_eval.py ++++ b/tests/test_lib_rl_safe_eval.py +@@ -1,6 +1,6 @@ + #Copyright ReportLab Europe Ltd. 2000-2017 + #see license.txt for license details +-"""Tests for reportlab.lib.rl_eval ++"""Tests for reportlab.lib.rl_safe_eval + """ + __version__='3.5.33' + from reportlab.lib.testutils import setOutDir,makeSuiteForClasses, printLocation +@@ -10,7 +10,7 @@ import reportlab + from reportlab import rl_config + import unittest + from reportlab.lib import colors +-from reportlab.lib.utils import rl_safe_eval, rl_safe_exec, annotateException ++from reportlab.lib.utils import rl_safe_eval, rl_safe_exec, annotateException, rl_extended_literal_eval + from reportlab.lib.rl_safe_eval import BadCode + + testObj = [1,('a','b',2),{'A':1,'B':2.0},"32"] +@@ -52,7 +52,6 @@ class SafeEvalTestSequenceMeta(type): + 'dict(a=1).get("a",2)', + 'dict(a=1).pop("a",2)', + '{"_":1+_ for _ in (1,2)}.pop(1,None)', +- '(type(1),type(str),type(testObj),type(TestClass))', + '1 if True else "a"', + '1 if False else "a"', + 'testFunc(bad=False)', +@@ -74,6 +73,8 @@ class SafeEvalTestSequenceMeta(type): + ( + 'fail', + ( ++ 'vars()', ++ '(type(1),type(str),type(testObj),type(TestClass))', + 'open("/tmp/myfile")', + 'SafeEvalTestCase.__module__', + ("testInst.__class__.__bases__[0].__subclasses__()",dict(g=dict(testInst=testInst))), +@@ -97,6 +98,8 @@ class SafeEvalTestSequenceMeta(type): + 'testFunc(bad=True)', + 'getattr(testInst,"__class__",14)', + '"{1}{2}".format(1,2)', ++ 'builtins', ++ '[ [ [ [ ftype(ctype(0, 0, 0, 0, 3, 67, b"t\\x00d\\x01\\x83\\x01\\xa0\\x01d\\x02\\xa1\\x01\\x01\\x00d\\x00S\\x00", (None, "os", "touch /tmp/exploited"), ("__import__", "system"), (), "<stdin>", "", 1, b"\\x12\\x01"), {})() for ftype in [type(lambda: None)] ] for ctype in [type(getattr(lambda: {None}, Word("__code__")))] ] for Word in [orgTypeFun("Word", (str,), { "mutated": 1, "startswith": lambda self, x: False, "__eq__": lambda self,x: self.mutate() and self.mutated < 0 and str(self) == x, "mutate": lambda self: {setattr(self, "mutated", self.mutated - 1)}, "__hash__": lambda self: hash(str(self)) })] ] for orgTypeFun in [type(type(1))]] and "red"', + ) + ), + ): +@@ -155,8 +158,46 @@ class SafeEvalTestBasics(unittest.TestCase): + def test_002(self): + self.assertTrue(rl_safe_eval("GA=='ga'")) + ++class ExtendedLiteralEval(unittest.TestCase): ++ def test_001(self): ++ S = colors.getAllNamedColors().copy() ++ C = {s:getattr(colors,s) for s in '''Blacker CMYKColor CMYKColorSep Color ColorType HexColor PCMYKColor PCMYKColorSep Whiter ++ _chooseEnforceColorSpace _enforceCMYK _enforceError _enforceRGB _enforceSEP _enforceSEP_BLACK ++ _enforceSEP_CMYK _namedColors _re_css asNative cmyk2rgb cmykDistance color2bw colorDistance ++ cssParse describe fade fp_str getAllNamedColors hsl2rgb hue2rgb linearlyInterpolatedColor ++ obj_R_G_B opaqueColor rgb2cmyk setColors toColor toColorOrNone'''.split() ++ if callable(getattr(colors,s,None))} ++ def showVal(s): ++ try: ++ r = rl_extended_literal_eval(s,C,S) ++ except: ++ r = str(sys.exc_info()[1]) ++ return r ++ ++ for expr, expected in ( ++ ('1.0', 1.0), ++ ('1', 1), ++ ('red', colors.red), ++ ('True', True), ++ ('False', False), ++ ('None', None), ++ ('Blacker(red,0.5)', colors.Color(.5,0,0,1)), ++ ('PCMYKColor(21,10,30,5,spotName="ABCD")', colors.PCMYKColor(21,10,30,5,spotName='ABCD',alpha=100)), ++ ('HexColor("#ffffff")', colors.Color(1,1,1,1)), ++ ('linearlyInterpolatedColor(red, blue, 0, 1, 0.5)', colors.Color(.5,0,.5,1)), ++ ('red.rgb()', 'Bad expression'), ++ ('__import__("sys")', 'Bad expression'), ++ ('globals()', 'Bad expression'), ++ ('locals()', 'Bad expression'), ++ ('vars()', 'Bad expression'), ++ ('builtins', 'Bad expression'), ++ ('__file__', 'Bad expression'), ++ ('__name__', 'Bad expression'), ++ ): ++ self.assertEqual(showVal(expr),expected,f"rl_extended_literal_eval({expr!r}) is not equal to expected {expected}") ++ + def makeSuite(): +- return makeSuiteForClasses(SafeEvalTestCase,SafeEvalTestBasics) ++ return makeSuiteForClasses(SafeEvalTestCase,SafeEvalTestBasics,ExtendedLiteralEval) + + if __name__ == "__main__": #noruntests + unittest.TextTestRunner().run(makeSuite()) diff -Nru python-reportlab-3.6.12/debian/patches/series python-reportlab-3.6.12/debian/patches/series --- python-reportlab-3.6.12/debian/patches/series 2021-03-13 12:29:10.000000000 +0000 +++ python-reportlab-3.6.12/debian/patches/series 2024-10-12 17:14:35.000000000 +0000 @@ -2,3 +2,4 @@ reproducible-build.patch toColor.patch reportlab-version.diff +0005-CVE-2023-33733-RCE-via-crafted-PDF-file.patch diff -Nru python-reportlab-3.6.12/debian/salsa-ci.yml python-reportlab-3.6.12/debian/salsa-ci.yml --- python-reportlab-3.6.12/debian/salsa-ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-reportlab-3.6.12/debian/salsa-ci.yml 2024-10-12 17:14:35.000000000 +0000 @@ -0,0 +1,12 @@ +# For more information on what jobs are run see: +# https://salsa.debian.org/salsa-ci-team/pipeline +# +# To enable the jobs, go to your repository (at salsa.debian.org) +# and click over Settings > CI/CD > Expand (in General pipelines). +# In "CI/CD configuration file" write debian/salsa-ci.yml and click +# in "Save Changes". The CI tests will run after the next commit. +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml +variables: + RELEASE: 'bookworm'
signature.asc
Description: This is a digitally signed message part.