CVE-2023-3490 (published 2023-06-30T22:15Z)

 SQL Injection in GitHub repository fossbilling/fossbilling prior to 0.5.3.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-89
cwe_name: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
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.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/fossbilling/fossbilling
commit_details: repo_url: https://github.com/fossbilling/fossbilling
author: Jaap Marcus
author_date: 2023-06-30 22:51:50+02:00
msg: Limit "LIMIT" to numbers only + Disable upload theme  (#1392)

* Prevent non numeric values being used in limits

Potential abuse for Sql injection
Only allow integers to be used


Adjust exception

* Disable upload assets via Theme pages

File upload was removed in an earlier PR

* Make sure the test run fine

* Fix the tests

* Use limit instead of per_page

* And another fix

---------

Co-authored-by: Belle Aerni <belleaerni@gmail.com>
repository: repo_url: https://github.com/fossbilling/fossbilling
repo_name: FOSSBilling/FOSSBilling
description: Empower your hosting business with FOSSBilling, the free and open-source solution for efficient billing and client management.
repo_language: PHP
owner: fossbilling
file_changes: filename: Pagination.php
change_type: ModificationType.MODIFY
diff: @@ -33,17 +33,24 @@ class Box_Pagination implements InjectionAwareInterface
         return $this->per_page;
     }
 
-    public function getSimpleResultSet($q, $values, $per_page = 100, $page = null)
+    public function getSimpleResultSet($q, $values, ?int $per_page = 100, ?int $page = null)
     {
         if (is_null($page)){
             $page = $_GET['page'] ?? 1;
         }
-        $per_page = $_GET['per_page'] ?? $per_page;
+        $per_page = $_GET['per_page'] ?? $per_page ?? 100;
+
+        if (!is_numeric($page) || $page < 1 ){
+           throw new \Box_Exception('Invalid page number');
+        }
+        if (!is_numeric($per_page) || $per_page < 1 ){
+           throw new \Box_Exception('Invalid per page number');
+        }
 
         $offset = ($page - 1) * $per_page;
 
         $sql = $q;
-        $sql .= sprintf(' LIMIT %s,%s', $offset, $per_page);
+        $sql .= sprintf(' LIMIT %u,%u', $offset, $per_page);
         $result = $this->di['db']->getAll($sql, $values);
 
         $exploded = explode('FROM', $q);
@@ -60,14 +67,21 @@ class Box_Pagination implements InjectionAwareInterface
         );
     }
 
-    public function getAdvancedResultSet($q, $values, $per_page = 100)
+    public function getAdvancedResultSet($q, $values, ?int $per_page = 100)
     {
-        $page = $page = $_GET['page'] ?? 1;
-        $per_page = $_GET['per_page'] ?? $per_page;
+        $page = $_GET['page'] ?? 1;
+        $per_page = $_GET['per_page'] ?? $per_page ?? 100;
+
+        if (!is_numeric($page) || $page < 1 ){
+           throw new \Box_Exception('Invalid page number');
+        }
+        if (!is_numeric($per_page) || $per_page < 1 ){
+           throw new \Box_Exception('Invalid per page number');
+        }
 
         $offset = ($page - 1) * $per_page;
         $q = str_replace('SELECT ', 'SELECT SQL_CALC_FOUND_ROWS ', $q);
-        $q .= sprintf(' LIMIT %s,%s', $offset, $per_page);
+        $q .= sprintf(' LIMIT %u,%u', $offset, $per_page);
         $result = $this->di['db']->getAll($q, $values);
         $total = $this->di['db']->getCell('SELECT FOUND_ROWS();');
 

programming_language: PHP
filename: Service.php
change_type: ModificationType.MODIFY
diff: @@ -177,8 +177,12 @@ class Service implements InjectionAwareInterface
     public function getPairs($data)
     {
         $limit = $data['per_page'] ?? 30;
+        if (!is_numeric($limit) || $limit < 1 ){
+            throw new \Box_Exception('Invalid per page number');
+        }
+
         [$sql, $params] = $this->getSearchQuery($data, "SELECT c.id, CONCAT_WS('', c.first_name,  ' ', c.last_name) as full_name");
-        $sql = $sql . ' LIMIT ' . $limit;
+        $sql .= sprintf(' LIMIT %u', $limit);
 
         return $this->di['db']->getAssoc($sql, $params);
     }

programming_language: PHP
filename: Admin.php
change_type: ModificationType.MODIFY
diff: @@ -63,7 +63,6 @@ class Admin implements \FOSSBilling\InjectionAwareInterface
                 throw new \Box_Exception('Theme ":name" assets folder is not writable. Files can not be uploaded and settings can not be saved. Set folder permissions to 777', [':name' => $t->getName()]);
             }
             $service->updateSettings($t, $preset, $_POST);
-            $service->uploadAssets($t, $_FILES);
             $service->regenerateThemeCssAndJsFiles($t, $preset, $api);
         } catch (\Exception $e) {
             error_log($e);

programming_language: PHP
filename: Service.php
change_type: ModificationType.MODIFY
diff: @@ -161,24 +161,6 @@ class Service implements InjectionAwareInterface
         }
     }
 
-    public function uploadAssets(Model\Theme $theme, array $files)
-    {
-        $dest = $theme->getPathAssets() . DIRECTORY_SEPARATOR;
-
-        foreach ($files as $filename => $f) {
-            if (UPLOAD_ERR_NO_FILE == $f['error']) {
-                continue;
-            }
-
-            $filename = str_replace('_', '.', $filename);
-            if (UPLOAD_ERR_OK != $f['error']) {
-                throw new \Box_Exception('Error uploading file :file Error code: :error', [':file' => $filename, ':error' => $f['error']]);
-            }
-
-            move_uploaded_file($f['tmp_name'], $dest . $filename);
-        }
-    }
-
     public function updateSettings(Model\Theme $theme, $preset, array $params)
     {
         $meta = $this->di['db']->findOne(

programming_language: PHP
filename: ServiceTest.php
change_type: ModificationType.MODIFY
diff: @@ -34,26 +34,6 @@ class Box_Mod_Theme_ServiceTest extends BBDbApiTestCase
         $this->assertIsArray($result);
     }
 
-    public function testuploadAssets()
-    {
-        $files = array(
-            'file1' => array(
-                    'error' => UPLOAD_ERR_NO_FILE,
-                ),
-            'file2' => array(
-                'error' => UPLOAD_ERR_OK,
-                'tmp_name' => 'tmpName',
-            ),
-
-        );
-
-        $service = new \Box\Mod\Theme\Service();
-        $service->setDi($this->di);
-
-        $themeModel = $service->getTheme('huraga');
-        $service->uploadAssets($themeModel, $files);
-    }
-
     public function testupdateSettings()
     {
         $service = new \Box\Mod\Theme\Service();
@@ -111,4 +91,4 @@ class Box_Mod_Theme_ServiceTest extends BBDbApiTestCase
         $this->assertInstanceOf('\Box\Mod\Theme\Model\Theme', $result);
         $this->assertEquals('huraga', $result->getName());
     }
-}
\ No newline at end of file
+}

programming_language: PHP
filename: AdminTest.php
change_type: ModificationType.MODIFY
diff: @@ -107,8 +107,6 @@ class AdminTest extends \BBTestCase {
             ->method('setCurrentThemePreset');
         $themeServiceMock->expects($this->atLeastOnce())
             ->method('updateSettings');
-        $themeServiceMock->expects($this->atLeastOnce())
-            ->method('uploadAssets');
         $themeServiceMock->expects($this->atLeastOnce())
             ->method('regenerateThemeCssAndJsFiles');
         $themeServiceMock->expects($this->atLeastOnce())
@@ -201,4 +199,3 @@ class AdminTest extends \BBTestCase {
     }
 
 }
- 
\ No newline at end of file

programming_language: PHP
filename: ServiceTest.php
change_type: ModificationType.MODIFY
diff: @@ -375,36 +375,5 @@ class ServiceTest extends \BBTestCase {
         $this->assertEquals('huraga', $result);
     }
 
-    public function testuploadAssets()
-    {
-        $themeMock = $this->getMockBuilder('\Box\Mod\Theme\Model\Theme')->disableOriginalConstructor()->getMock();
-        $themeMock->expects($this->atLeastOnce())
-            ->method('getPathAssets');
-        $files = array(
-            'test2' => array(
-                'error' => UPLOAD_ERR_NO_FILE
-                ),
-            'test1' => array(
-                    'error' => UPLOAD_ERR_OK,
-                    'tmp_name' => 'tempName',
-                ),
-        );
-        $this->service->uploadAssets($themeMock, $files);
-    }
-
-    public function testuploadAssets_Exception()
-    {
-        $themeMock = $this->getMockBuilder('\Box\Mod\Theme\Model\Theme')->disableOriginalConstructor()->getMock();
-        $themeMock->expects($this->atLeastOnce())
-            ->method('getPathAssets');
-        $files = array(
-            'test0' => array(
-                'error' => UPLOAD_ERR_CANT_WRITE
-            ),
-        );
-        $this->expectException(\Box_Exception::class);
-        $this->expectExceptionMessage(sprintf("Error uploading file %s Error code: %d", 'test0', UPLOAD_ERR_CANT_WRITE));
-        $this->service->uploadAssets($themeMock, $files);
-    }
 
 }

programming_language: PHP