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'

Attachment: signature.asc
Description: This is a digitally signed message part.

Reply via email to