Skip to content

CSRF in Category Report preferences allows deleting or duplicating configurations via GET

Moderate
Fasse published GHSA-mvx3-m6p6-7r9w Jul 20, 2026

Package

composer admidio/admidio (Composer)

Affected versions

<= 5.0.10

Patched versions

>= 5.0.11

Description

Summary

modules/category-report/preferences.php performs persistent Category Report configuration changes from GET parameters without validating an adm_csrf_token. An attacker can cause an authenticated administrator to visit a crafted URL and delete or duplicate Category Report configurations.

Credit requested: Yaohui Wang

Affected repository / versions

Repository: https://github.com/Admidio/admidio
Observed in:

  • master at e1fe6fd
  • released tag v5.0.10
  • released tag v5.0.9

Package: composer admidio/admidio

Vulnerable code paths

modules/category-report/preferences.php:

require_once(__DIR__ . '/../../system/login_valid.php');

if (!$gCurrentUser->isAdministrator()) {
    throw new Exception('SYS_NO_RIGHTS');
}

$getAdd = admFuncVariableIsValid($_GET, 'add', 'bool');
$getDelete = admFuncVariableIsValid($_GET, 'delete', 'numeric', array('defaultValue' => 0));
$getCopy = admFuncVariableIsValid($_GET, 'copy', 'numeric', array('defaultValue' => 0));

if ($getDelete > 0 && isset($config[$getDelete - 1])) {
    $config[$getDelete - 1]['id'] = $config[$getDelete - 1]['id'] * (-1);
    $config = $report->saveConfigArray($config);
}

if ($getCopy > 0) {
    $config[] = array(...);
    $config = $report->saveConfigArray($config);
}

There is no call to SecurityUtils::validateCsrfToken() and no form-object CSRF validation before the state-changing delete and copy branches.

system/classes/CategoryReport.php confirms that saveConfigArray() persists these changes:

public function saveConfigArray(array $arrConfiguration): array
{
    $gDb->startTransaction();
    foreach ($arrConfiguration as $values) {
        if ($values['id'] === '' || $values['id'] > 0) {
            $categoryReport = new Entity($gDb, TBL_CATEGORY_REPORT, 'crt', $values['id']);
            ...
            $categoryReport->save();
            $gSettingsManager->set('category_report_default_configuration', $defaultConfiguration);
        } else {
            $values['id'] = $values['id'] * (-1);
            $categoryReport = new Entity($gDb, TBL_CATEGORY_REPORT, 'crt', $values['id']);
            $categoryReport->delete();
        }
    }
    $gDb->endTransaction();
}

Impact

An attacker who can get an authenticated administrator to perform a top-level navigation can trigger unauthorized Category Report configuration changes:

  • GET /modules/category-report/preferences.php?delete=1 deletes the first persisted configuration if present.
  • GET /modules/category-report/preferences.php?copy=1 duplicates the first persisted configuration if present.

This affects integrity and availability of the Category Report module configuration. The server-side delete branch does not require the UI-generated delete link and does not enforce the UI limitation that only non-default configurations are shown with a delete button; direct requests are accepted if the referenced index exists.

Admidio session cookies are configured with SameSite=Lax (src/Session/Entity/Session.php), which still allows cookies on top-level cross-site GET navigations. Therefore, a normal link/form/window navigation is sufficient for CSRF; a token is still needed for these state-changing actions.

Minimal proof of concept

Prerequisites:

  1. Category Report module is enabled.
  2. The victim is logged in as an Admidio administrator.
  3. At least one Category Report configuration exists.

A malicious page can trigger the delete action with a top-level navigation:

<!doctype html>
<html>
  <body>
    <a href="https://victim.example/modules/category-report/preferences.php?delete=1">View report</a>
    <script>
      // User interaction / top-level navigation variant:
      // location.href = "https://victim.example/modules/category-report/preferences.php?delete=1";
    </script>
  </body>
</html>

Expected result when visited by an authenticated administrator: preferences.php enters the $getDelete branch and CategoryReport::saveConfigArray() deletes the selected row from adm_category_report without any CSRF token.

A copy/clutter variant is also possible:

<a href="https://victim.example/modules/category-report/preferences.php?copy=1">View report</a>

Suggested fix

Move the persistent delete and copy operations behind POST-only handlers and require adm_csrf_token validation before calling CategoryReport::saveConfigArray(). Alternatively, require SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']) and reject GET for all persistent configuration changes in modules/category-report/preferences.php.

It would also be safer to enforce the server-side business rule that default / last remaining configurations cannot be deleted, instead of relying only on whether the UI renders a delete URL.

Suggested classification

CWE-352: Cross-Site Request Forgery (CSRF)

Suggested severity: Medium, although Low is also defensible depending on whether the authenticated administrator victim is modeled as PR:H. The vulnerability requires admin login state and user interaction, but it performs persistent configuration changes without token validation.

Severity

Moderate

CVE ID

No known CVE

Weaknesses

Cross-Site Request Forgery (CSRF)

The web application does not, or cannot, sufficiently verify whether a request was intentionally provided by the user who sent the request, which could have originated from an unauthorized actor. Learn more on MITRE.

Credits