CVE-2021-43608: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value: Doctrine DBAL 3.x before 3.1.4 allows SQL Injection. The escaping of offset and length inputs to the generation of a LIMIT clause was not probably cast to an integer, allowing SQL injection to take place if application developers passed unescaped user input to the DBAL QueryBuilder or any other API that ultimately uses the AbstractPlatform::modifyLimitQuery API.
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: 2021-12-09T20:15Z
Repository: https://github.com/doctrine/dbal
Language: PHP
Files changed: src/Platforms/AbstractPlatform.php, tests/Platforms/AbstractPlatformTestCase.php, tests/Platforms/DB2PlatformTest.php, tests/Platforms/OraclePlatformTest.php
Commit date: 2021-11-10 11:47:29 -0800
Commit message: Cast LIMIT and OFFSET to int when building limit query

Diff: diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php
index e7f0819b5..f22fab5bc 100644
--- a/src/Platforms/AbstractPlatform.php
+++ b/src/Platforms/AbstractPlatform.php
@@ -3363,11 +3363,15 @@ abstract class AbstractPlatform
                 'Platform %s does not support offset values in limit queries.',
                 $this->getName()
             ));
         }
 
-        return $this->doModifyLimitQuery($query, $limit, $offset);
+        if ($limit !== null) {
+            $limit = (int) $limit;
+        }
+
+        return $this->doModifyLimitQuery($query, $limit, (int) $offset);
     }
 
     /**
      * Adds an platform-specific LIMIT clause to the query.
      *
diff --git a/tests/Platforms/AbstractPlatformTestCase.php b/tests/Platforms/AbstractPlatformTestCase.php
index b5cfa25b1..1d648ec6a 100644
--- a/tests/Platforms/AbstractPlatformTestCase.php
+++ b/tests/Platforms/AbstractPlatformTestCase.php
@@ -1468,10 +1468,23 @@ abstract class AbstractPlatformTestCase extends TestCase
             $query,
             $this->platform->modifyLimitQuery($query, null, 0)
         );
     }
 
+    public function testLimitOffsetCastToInt(): void
+    {
+        self::assertSame(
+            $this->getLimitOffsetCastToIntExpectedQuery(),
+            $this->platform->modifyLimitQuery('SELECT * FROM user', '1 BANANA', '2 APPLES')
+        );
+    }
+
+    protected function getLimitOffsetCastToIntExpectedQuery(): string
+    {
+        return 'SELECT * FROM user LIMIT 1 OFFSET 2';
+    }
+
     /**
      * @param array<string, mixed> $column
      *
      * @dataProvider asciiStringSqlDeclarationDataProvider
      */
diff --git a/tests/Platforms/DB2PlatformTest.php b/tests/Platforms/DB2PlatformTest.php
index fe07ca6f6..c8d0f2560 100644
--- a/tests/Platforms/DB2PlatformTest.php
+++ b/tests/Platforms/DB2PlatformTest.php
@@ -771,6 +771,12 @@ class DB2PlatformTest extends AbstractPlatformTestCase
         self::assertStringContainsStringIgnoringCase(
             "'Foo''Bar\\'",
             $this->platform->getListTableForeignKeysSQL("Foo'Bar\\")
         );
     }
+
+    protected function getLimitOffsetCastToIntExpectedQuery(): string
+    {
+        return 'SELECT db22.* FROM (SELECT db21.*, ROW_NUMBER() OVER() AS DC_ROWNUM'
+            . ' FROM (SELECT * FROM user) db21) db22 WHERE db22.DC_ROWNUM >= 3 AND db22.DC_ROWNUM <= 3';
+    }
 }
diff --git a/tests/Platforms/OraclePlatformTest.php b/tests/Platforms/OraclePlatformTest.php
index 88a88147b..e0f1e6d16 100644
--- a/tests/Platforms/OraclePlatformTest.php
+++ b/tests/Platforms/OraclePlatformTest.php
@@ -984,6 +984,12 @@ SQL
         return [
             ['VARCHAR2(12)', ['length' => 12]],
             ['CHAR(12)', ['length' => 12, 'fixed' => true]],
         ];
     }
+
+    protected function getLimitOffsetCastToIntExpectedQuery(): string
+    {
+        return 'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT * FROM user) a WHERE ROWNUM <= 3)'
+            . ' WHERE doctrine_rownum >= 3';
+    }
 }

Vulnerable code:         return $this->doModifyLimitQuery($query, $limit, $offset);
Fixed code:         if ($limit !== null) {
            $limit = (int) $limit;
        }

        return $this->doModifyLimitQuery($query, $limit, (int) $offset);
    public function testLimitOffsetCastToInt(): void
    {
        self::assertSame(
            $this->getLimitOffsetCastToIntExpectedQuery(),
            $this->platform->modifyLimitQuery('SELECT * FROM user', '1 BANANA', '2 APPLES')
        );
    }

    protected function getLimitOffsetCastToIntExpectedQuery(): string
    {
        return 'SELECT * FROM user LIMIT 1 OFFSET 2';
    }


    protected function getLimitOffsetCastToIntExpectedQuery(): string
    {
        return 'SELECT db22.* FROM (SELECT db21.*, ROW_NUMBER() OVER() AS DC_ROWNUM'
            . ' FROM (SELECT * FROM user) db21) db22 WHERE db22.DC_ROWNUM >= 3 AND db22.DC_ROWNUM <= 3';
    }

    protected function getLimitOffsetCastToIntExpectedQuery(): string
    {
        return 'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT * FROM user) a WHERE ROWNUM <= 3)'
            . ' WHERE doctrine_rownum >= 3';
    }