Hi,

On Thu, Oct 13, 2022 at 09:13:18PM +0200, Moritz Mühlenhoff wrote:
> Source: lava
> X-Debbugs-CC: t...@security.debian.org
> Severity: grave
> Tags: security
> 
> Hi,
> 
> The following vulnerability was published for lava.
> 
> CVE-2022-42902[0]:
> | In Linaro Automated Validation Architecture (LAVA) before 2022.10,
> | there is dynamic code execution in lava_server/lavatable.py. Due to
> | improper input sanitization, an anonymous user can force the lava-
> | server-gunicorn service to execute user-provided code on the server.
> 
> https://git.lavasoftware.org/lava/lava/-/merge_requests/1834
> https://git.lavasoftware.org/lava/lava/-/commit/e66b74cd6c175ff8826b8f3431740963be228b52?merge_request_iid=1834
> 
> If you fix the vulnerability please also make sure to include the
> CVE (Common Vulnerabilities & Exposures) id in your changelog entry.
> 
> For further information see:
> 
> [0] https://security-tracker.debian.org/tracker/CVE-2022-42902
>     https://www.cve.org/CVERecord?id=CVE-2022-42902
> 
> Please adjust the affected versions in the BTS as needed.

I have uploaded a fix version to unstable (latest upstream), and I would
like to upload the attached debdiff to -security. That package builds
cleanly and passes its autopkgtest on bullseye. Let me know.

I'm also attaching the 2 patches included directly, since they are
easier to read than the diff-in-diff. The main patch for the security
issue is exactly the one that was applied upstream.
From e66b74cd6c175ff8826b8f3431740963be228b52 Mon Sep 17 00:00:00 2001
From: Igor Ponomarev <igor.ponoma...@collabora.com>
Date: Mon, 26 Sep 2022 18:51:47 +0300
Subject: [PATCH] Replace dynamic code execution in lava_server/lavatable.py

`exec` was used to create query parameters for the tables searches.
`exec` is extremely dangerous and can lead to remote code execution.
It is also very slow.
The new implementation is equivalent in function but uses
dictionaries and unpacking to create arguments.
---
 lava_server/lavatable.py | 76 ++++++++++++++--------------------------
 1 file changed, 26 insertions(+), 50 deletions(-)

--- a/lava_server/lavatable.py
+++ b/lava_server/lavatable.py
@@ -41,8 +41,6 @@ class LavaView(tables.SingleTableView):
         """
         bespoke time-based field handling
         """
-        local_namespace = locals()
-        local_namespace["q"] = query
         time_queries = {}
         if hasattr(self.table_class.Meta, "times"):
             # filter the possible list by the request
@@ -50,26 +48,20 @@ class LavaView(tables.SingleTableView):
                 # check if the request includes the current time filter & get the value
                 match = self.request.GET.get(key)
                 if match and match != "":
-                    self.terms[key] = "%s within %s %s" % (
-                        key,
-                        match,
-                        value,
-                    )  # the label for this query in the search list
+                    self.terms[key] = f"{key} within {match} {value}"
+                    # the label for this query in the search list
                     time_queries[key] = value
             for key, value in time_queries.items():
                 match = escape(self.request.GET.get(key))
                 # escape converts None into u'None'
                 if not match or match == "" or match == "None":
                     continue
-                args = "q = q.__and__(Q({0}__gte=timezone.now()-timedelta({1}={2})))".format(
-                    key, value, match
+
+                query &= Q(
+                    **{f"{key}__gte": timezone.now() - timedelta(**{value: int(match)})}
                 )
-                try:
-                    exec(args, globals(), local_namespace)  # sets the value of q
-                except SyntaxError:
-                    # should log the exception somewhere...
-                    continue  # just skip this term - results in a query matching All.
-        return local_namespace["q"]
+
+        return query
 
     def get_table_data(self, prefix=None):
         """
@@ -145,60 +137,44 @@ class LavaView(tables.SingleTableView):
         if not self.request:
             return data
 
-        local_namespace = locals()
-        local_namespace["q"] = Q()
+        q = Q()
         self.terms = {}
         # discrete searches
         for key, val in distinct.items():
             if key in self.table_class.Meta.searches:
-                args = 'q = q.__and__(Q({0}__contains="{1}"))'.format(key, val)
-                try:
-                    exec(args, globals(), local_namespace)  # sets the value of q
-                except SyntaxError:
-                    # should log exception somewhere...
-                    continue  # just skip this term - results in a query matching All.
+                q &= Q(**{f"{key}__contains": val})
+
             if (
                 hasattr(self.table_class.Meta, "queries")
                 and key in self.table_class.Meta.queries.keys()
             ):
-                # note that this calls the function 'key' with the argument from the search
-                args = 'q = q.__and__(self.{0}("{1}"))'.format(key, val)
-                try:
-                    exec(args, globals(), local_namespace)
-                except SyntaxError:
-                    # should log exception somewhere...
-                    continue
+                # note that this calls
+                # the function 'key' with the argument from the search
+                q &= getattr(self, key)(val)
+
         # general OR searches
         if self.request.GET.get(table_search):
             self.terms["search"] = escape(self.request.GET.get(table_search))
         if hasattr(self.table_class.Meta, "searches") and "search" in self.terms:
             for key, val in self.table_class.Meta.searches.items():
-                # this is a little bit of magic - creates an OR clause in the query based
-                # on the iterable search hash passed in via the table_class
+                # this is a little bit of magic - creates an OR clause
+                # in the query based on the iterable search hash
+                # passed in via the table_class
                 # e.g. self.searches = {'id', 'contains'}
-                # so every simple search column in the table is queried at the same time with OR
-                args = 'q = q.__or__(Q({0}__{1}=self.terms["search"]))'.format(key, val)
-                try:
-                    exec(args, globals(), local_namespace)  # sets the value of q
-                except SyntaxError:
-                    # should log exception somewhere...
-                    continue  # just skip this term - results in a query matching All.
+                # so every simple search column in the table
+                # is queried at the same time with OR
+                q |= Q(**{f"{key}__{val}": self.terms["search"]})
+
             # call explicit handlers as simple text searches of relational fields.
             if hasattr(self.table_class.Meta, "queries"):
                 for key in self.table_class.Meta.queries:
-                    # note that this calls the function 'key' with the argument from the search
-                    args = 'q = q.__or__(self.{0}("{1}"))'.format(
-                        key, self.terms["search"].encode("utf-8")
-                    )
-                    try:
-                        exec(args, globals(), local_namespace)
-                    except SyntaxError:
-                        # should log exception somewhere...
-                        continue
+                    # note that this calls the function 'key'
+                    # with the argument from the search
+                    q |= getattr(self, key)(self.terms["search"])
+
         # now add "class specials" - from an iterable hash
         # datetime uses (start_time__lte=timezone.now()-timedelta(days=3)
-        data = data.filter(self._time_filter(local_namespace["q"]))
-        return data
+        return data.filter(self._time_filter(q))
 
 
 class LavaTable(tables.Table):
From e1f934df702a1db386e301fbe2d51ab4154ee21c Mon Sep 17 00:00:00 2001
From: Antonio Terceiro <antonio.terce...@linaro.org>
Date: Tue, 18 Oct 2022 17:57:46 -0300
Subject: [PATCH] share/requires.py: fix building for debian -backports and
 -security suites

---
 share/requires.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/share/requires.py b/share/requires.py
index 2599028d8..00aadc3a4 100755
--- a/share/requires.py
+++ b/share/requires.py
@@ -123,6 +123,8 @@ def main():
         help="Distribution package names for unittest support - requires --names",
     )
     args = parser.parse_args()
+    args.suite = args.suite.replace("-backports", "")
+    args.suite = args.suite.replace("-security", "")
     if args.unittests and not args.names:
         raise RuntimeError("--unittests option requires --names")
     try:
-- 
2.35.1

diff -Nru lava-2020.12/debian/changelog lava-2020.12/debian/changelog
--- lava-2020.12/debian/changelog	2021-05-24 09:05:18.000000000 -0300
+++ lava-2020.12/debian/changelog	2022-10-18 17:24:50.000000000 -0300
@@ -1,3 +1,10 @@
+lava (2020.12-5+deb111u1) bullseye-security; urgency=high
+
+  * Fix remote code execution [CVE-2022-42902] (Closes: #1021737)
+  * Add patch to fix building the package for -security
+
+ -- Antonio Terceiro <terce...@debian.org>  Tue, 18 Oct 2022 17:24:50 -0300
+
 lava (2020.12-5) unstable; urgency=medium
 
   * Fix PyYAML usage after backwards-incompatible security update
diff -Nru lava-2020.12/debian/patches/0003-Replace-dynamic-code-execution-in-lava_server-lavata.patch lava-2020.12/debian/patches/0003-Replace-dynamic-code-execution-in-lava_server-lavata.patch
--- lava-2020.12/debian/patches/0003-Replace-dynamic-code-execution-in-lava_server-lavata.patch	1969-12-31 21:00:00.000000000 -0300
+++ lava-2020.12/debian/patches/0003-Replace-dynamic-code-execution-in-lava_server-lavata.patch	2022-10-18 17:24:33.000000000 -0300
@@ -0,0 +1,139 @@
+From e66b74cd6c175ff8826b8f3431740963be228b52 Mon Sep 17 00:00:00 2001
+From: Igor Ponomarev <igor.ponoma...@collabora.com>
+Date: Mon, 26 Sep 2022 18:51:47 +0300
+Subject: [PATCH] Replace dynamic code execution in lava_server/lavatable.py
+
+`exec` was used to create query parameters for the tables searches.
+`exec` is extremely dangerous and can lead to remote code execution.
+It is also very slow.
+The new implementation is equivalent in function but uses
+dictionaries and unpacking to create arguments.
+---
+ lava_server/lavatable.py | 76 ++++++++++++++--------------------------
+ 1 file changed, 26 insertions(+), 50 deletions(-)
+
+--- a/lava_server/lavatable.py
++++ b/lava_server/lavatable.py
+@@ -41,8 +41,6 @@ class LavaView(tables.SingleTableView):
+         """
+         bespoke time-based field handling
+         """
+-        local_namespace = locals()
+-        local_namespace["q"] = query
+         time_queries = {}
+         if hasattr(self.table_class.Meta, "times"):
+             # filter the possible list by the request
+@@ -50,26 +48,20 @@ class LavaView(tables.SingleTableView):
+                 # check if the request includes the current time filter & get the value
+                 match = self.request.GET.get(key)
+                 if match and match != "":
+-                    self.terms[key] = "%s within %s %s" % (
+-                        key,
+-                        match,
+-                        value,
+-                    )  # the label for this query in the search list
++                    self.terms[key] = f"{key} within {match} {value}"
++                    # the label for this query in the search list
+                     time_queries[key] = value
+             for key, value in time_queries.items():
+                 match = escape(self.request.GET.get(key))
+                 # escape converts None into u'None'
+                 if not match or match == "" or match == "None":
+                     continue
+-                args = "q = q.__and__(Q({0}__gte=timezone.now()-timedelta({1}={2})))".format(
+-                    key, value, match
++
++                query &= Q(
++                    **{f"{key}__gte": timezone.now() - timedelta(**{value: int(match)})}
+                 )
+-                try:
+-                    exec(args, globals(), local_namespace)  # sets the value of q
+-                except SyntaxError:
+-                    # should log the exception somewhere...
+-                    continue  # just skip this term - results in a query matching All.
+-        return local_namespace["q"]
++
++        return query
+ 
+     def get_table_data(self, prefix=None):
+         """
+@@ -145,60 +137,44 @@ class LavaView(tables.SingleTableView):
+         if not self.request:
+             return data
+ 
+-        local_namespace = locals()
+-        local_namespace["q"] = Q()
++        q = Q()
+         self.terms = {}
+         # discrete searches
+         for key, val in distinct.items():
+             if key in self.table_class.Meta.searches:
+-                args = 'q = q.__and__(Q({0}__contains="{1}"))'.format(key, val)
+-                try:
+-                    exec(args, globals(), local_namespace)  # sets the value of q
+-                except SyntaxError:
+-                    # should log exception somewhere...
+-                    continue  # just skip this term - results in a query matching All.
++                q &= Q(**{f"{key}__contains": val})
++
+             if (
+                 hasattr(self.table_class.Meta, "queries")
+                 and key in self.table_class.Meta.queries.keys()
+             ):
+-                # note that this calls the function 'key' with the argument from the search
+-                args = 'q = q.__and__(self.{0}("{1}"))'.format(key, val)
+-                try:
+-                    exec(args, globals(), local_namespace)
+-                except SyntaxError:
+-                    # should log exception somewhere...
+-                    continue
++                # note that this calls
++                # the function 'key' with the argument from the search
++                q &= getattr(self, key)(val)
++
+         # general OR searches
+         if self.request.GET.get(table_search):
+             self.terms["search"] = escape(self.request.GET.get(table_search))
+         if hasattr(self.table_class.Meta, "searches") and "search" in self.terms:
+             for key, val in self.table_class.Meta.searches.items():
+-                # this is a little bit of magic - creates an OR clause in the query based
+-                # on the iterable search hash passed in via the table_class
++                # this is a little bit of magic - creates an OR clause
++                # in the query based on the iterable search hash
++                # passed in via the table_class
+                 # e.g. self.searches = {'id', 'contains'}
+-                # so every simple search column in the table is queried at the same time with OR
+-                args = 'q = q.__or__(Q({0}__{1}=self.terms["search"]))'.format(key, val)
+-                try:
+-                    exec(args, globals(), local_namespace)  # sets the value of q
+-                except SyntaxError:
+-                    # should log exception somewhere...
+-                    continue  # just skip this term - results in a query matching All.
++                # so every simple search column in the table
++                # is queried at the same time with OR
++                q |= Q(**{f"{key}__{val}": self.terms["search"]})
++
+             # call explicit handlers as simple text searches of relational fields.
+             if hasattr(self.table_class.Meta, "queries"):
+                 for key in self.table_class.Meta.queries:
+-                    # note that this calls the function 'key' with the argument from the search
+-                    args = 'q = q.__or__(self.{0}("{1}"))'.format(
+-                        key, self.terms["search"].encode("utf-8")
+-                    )
+-                    try:
+-                        exec(args, globals(), local_namespace)
+-                    except SyntaxError:
+-                        # should log exception somewhere...
+-                        continue
++                    # note that this calls the function 'key'
++                    # with the argument from the search
++                    q |= getattr(self, key)(self.terms["search"])
++
+         # now add "class specials" - from an iterable hash
+         # datetime uses (start_time__lte=timezone.now()-timedelta(days=3)
+-        data = data.filter(self._time_filter(local_namespace["q"]))
+-        return data
++        return data.filter(self._time_filter(q))
+ 
+ 
+ class LavaTable(tables.Table):
diff -Nru lava-2020.12/debian/patches/0004-share-requires.py-fix-building-for-debian-backports-.patch lava-2020.12/debian/patches/0004-share-requires.py-fix-building-for-debian-backports-.patch
--- lava-2020.12/debian/patches/0004-share-requires.py-fix-building-for-debian-backports-.patch	1969-12-31 21:00:00.000000000 -0300
+++ lava-2020.12/debian/patches/0004-share-requires.py-fix-building-for-debian-backports-.patch	2022-10-18 17:24:50.000000000 -0300
@@ -0,0 +1,26 @@
+From e1f934df702a1db386e301fbe2d51ab4154ee21c Mon Sep 17 00:00:00 2001
+From: Antonio Terceiro <antonio.terce...@linaro.org>
+Date: Tue, 18 Oct 2022 17:57:46 -0300
+Subject: [PATCH] share/requires.py: fix building for debian -backports and
+ -security suites
+
+---
+ share/requires.py | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/share/requires.py b/share/requires.py
+index 2599028d8..00aadc3a4 100755
+--- a/share/requires.py
++++ b/share/requires.py
+@@ -123,6 +123,8 @@ def main():
+         help="Distribution package names for unittest support - requires --names",
+     )
+     args = parser.parse_args()
++    args.suite = args.suite.replace("-backports", "")
++    args.suite = args.suite.replace("-security", "")
+     if args.unittests and not args.names:
+         raise RuntimeError("--unittests option requires --names")
+     try:
+-- 
+2.35.1
+
diff -Nru lava-2020.12/debian/patches/series lava-2020.12/debian/patches/series
--- lava-2020.12/debian/patches/series	2021-05-24 09:05:18.000000000 -0300
+++ lava-2020.12/debian/patches/series	2022-10-18 17:24:50.000000000 -0300
@@ -1,2 +1,4 @@
 0001-lava_rest_app-fix-field-name-in-filters.patch
 0002-lava_common.compat-add-support-for-PyYAML-5.4.patch
+0003-Replace-dynamic-code-execution-in-lava_server-lavata.patch
+0004-share-requires.py-fix-building-for-debian-backports-.patch

Attachment: signature.asc
Description: PGP signature

Reply via email to