CVE-2023-40954: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value: A SQL injection vulnerability in Grzegorz Marczynski Dynamic Progress Bar (aka web_progress) v. 11.0 through 11.0.2, v12.0 through v12.0.2, v.13.0 through v13.0.2, v.14.0 through v14.0.2.1, v.15.0 through v15.0.2, and v16.0 through v16.0.2.1 allows a remote attacker to gain privileges via the recency parameter in models/web_progress.py component.
CWE description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
CVSS v3 base score: 9.8
Published: 2023-12-15T01:15Z
Repository: https://github.com/gmarczynski/odoo-web-progress
Language: Python
Files changed: web_progress/models/web_progress.py, web_progress/tests/test_web_progress.py
Commit date: 2023-08-10 21:58:13 +0200
Commit message: [FIX] Escape parameter of SQL query, closes #34

Diff: diff --git a/web_progress/models/web_progress.py b/web_progress/models/web_progress.py
index 18de2ce..750dac3 100644
--- a/web_progress/models/web_progress.py
+++ b/web_progress/models/web_progress.py
@@ -163,21 +163,21 @@ class WebProgress(models.TransientModel):
         :param recency: (int) seconds back
         :return list of progress codes
         """
         query = """
         SELECT code, array_agg(state) FROM web_progress
-        WHERE create_date > timezone('utc', now()) - INTERVAL '{recency} SECOND'
+        WHERE create_date > timezone('utc', now()) - INTERVAL '%s SECOND'
               AND recur_depth = 0 {user_id}
         GROUP BY code
         """.format(
             recency=recency or 0,
             user_id=not self.is_progress_admin() and "AND create_uid = {user_id}"
                 .format(
                 user_id=self.env.user.id,
             ) or '')
         # superuser has right to see (and cancel) progress of everybody
-        self.env.cr.execute(query)
+        self.env.cr.execute(query, (recency, ))
         result = self.env.cr.fetchall()
         ret = [{
             'code': r[0],
         } for r in result if r[0] and 'cancel' not in r[1] and 'done' not in r[1]]
         return ret
diff --git a/web_progress/tests/test_web_progress.py b/web_progress/tests/test_web_progress.py
index b562421..e693881 100644
--- a/web_progress/tests/test_web_progress.py
+++ b/web_progress/tests/test_web_progress.py
@@ -1,7 +1,9 @@
 from odoo.tests import common, tagged
-from odoo import exceptions
+from odoo import exceptions, api, registry
+from odoo.tools import mute_logger
+from psycopg2.errors import SyntaxError
 import uuid
 import logging
 from ..models.web_progress import last_report_time
 
 _logger = logging.getLogger(__name__)
@@ -113,5 +115,34 @@ class WebProgressTest(common.SavepointCase):
         progress_code = str(uuid.uuid4())
         self.partner_ids = self.partner_ids.with_context(progress_code=progress_code)
         self.partner_ids.web_progress_percent(0, "Start")
         self.partner_ids.web_progress_percent(50, "Middle")
         self.partner_ids.web_progress_percent(100, "End")
+
+
+class WebProgressTestAllProgress(common.SavepointCase):
+    at_install = True
+    post_install = False
+
+    @mute_logger('odoo.sql_db')
+    def test_get_all_progress(self):
+        """
+        Check call to get_all_progress without and with parameters.
+        Verify if the parameter is properly escaped in the internal SQL query.
+        """
+        progress_code = str(uuid.uuid4())
+        partner_obj = self.env['res.partner'].with_context(progress_code=progress_code)
+        partner_obj.web_progress_percent(0, "Start")
+        with api.Environment.manage():
+            with registry(self.env.cr.dbname).cursor() as new_cr:
+                # Create a new environment with a new cursor
+                new_env = api.Environment(new_cr, self.env.uid, self.env.context)
+                progress_obj = self.env['web.progress'].with_env(new_env)
+                res = progress_obj.get_all_progress()
+                self.assertEqual(res, [{'code': progress_code}])
+                res = progress_obj.get_all_progress(0)
+                self.assertEqual(res, [])
+                with self.assertRaises(SyntaxError) as e:
+                    progress_obj.get_all_progress("0 SECOND' GROUP BY code; "
+                                                  "SELECT code, array_agg(state) FROM web_progress "
+                                                  "WHERE create_date > timezone('utc', now()) - INTERVAL '10")
+                new_cr.rollback()

Vulnerable code:         WHERE create_date > timezone('utc', now()) - INTERVAL '{recency} SECOND'
        self.env.cr.execute(query)
from odoo import exceptions
Fixed code:         WHERE create_date > timezone('utc', now()) - INTERVAL '%s SECOND'
        self.env.cr.execute(query, (recency, ))
from odoo import exceptions, api, registry
from odoo.tools import mute_logger
from psycopg2.errors import SyntaxError


class WebProgressTestAllProgress(common.SavepointCase):
    at_install = True
    post_install = False

    @mute_logger('odoo.sql_db')
    def test_get_all_progress(self):
        """
        Check call to get_all_progress without and with parameters.
        Verify if the parameter is properly escaped in the internal SQL query.
        """
        progress_code = str(uuid.uuid4())
        partner_obj = self.env['res.partner'].with_context(progress_code=progress_code)
        partner_obj.web_progress_percent(0, "Start")
        with api.Environment.manage():
            with registry(self.env.cr.dbname).cursor() as new_cr:
                # Create a new environment with a new cursor
                new_env = api.Environment(new_cr, self.env.uid, self.env.context)
                progress_obj = self.env['web.progress'].with_env(new_env)
                res = progress_obj.get_all_progress()
                self.assertEqual(res, [{'code': progress_code}])
                res = progress_obj.get_all_progress(0)
                self.assertEqual(res, [])
                with self.assertRaises(SyntaxError) as e:
                    progress_obj.get_all_progress("0 SECOND' GROUP BY code; "
                                                  "SELECT code, array_agg(state) FROM web_progress "
                                                  "WHERE create_date > timezone('utc', now()) - INTERVAL '10")
                new_cr.rollback()