GitHub Advisoryadvisory · Packagist
GHSA-7cx3-2qx2-3g6w phpMyFAQ's Missing Authorization on Tag Deletion Allows Any Authenticated User to Delete Tags
MEDIUM
Official page ↗
- Ecosystem
- Packagist
- Packages
- phpmyfaq/phpmyfaq, thorsten/phpmyfaq
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L
- Weaknesses
- CWE-862
- Reviewed
- 2026-05-06
Affected packages
• Packagist phpmyfaq/phpmyfaq: < 4.1.2 → fixed in 4.1.2
• Packagist thorsten/phpmyfaq: < 4.1.2 → fixed in 4.1.2
Description
Summary
The `TagController::delete()` endpoint at `DELETE /admin/api/content/tags/{tagId}` only verifies that the user is logged in (`userIsAuthenticated()`), but does not check any permission. Any authenticated user — including regular non-admin frontend users — can delete any tag by ID. This contrasts with `TagController::update()` and `TagController::search()`, which both enforce the `FAQ_EDIT` permission.
Details
In `phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/TagController.php`, the `delete()` method (line 121-133) uses only `$this->userIsAuthenticated()`:
```php
[Route(path: 'content/tags/{tagId}', name: 'admin.api.content.tags.id', methods: ['DELETE'])]
public function delete(Request $request): JsonResponse
{
$this->userIsAuthenticated(); // Only checks isLoggedIn() — no permission check
$tagId = (int) Filter::filterVar($request->attributes->get('tagId'), FILTER_VALIDATE_INT);
if ($this->tags->delete($tagId)) {
return $this->json(['success' => Translation::get(key: 'ad_tag_delete_success')], Response::HTTP_OK);
}
return $this->json(['error' => Translation::get(key: 'ad_tag_delete_error')], Response::HTTP_BAD_REQUEST);
}
```
Compare with `update()` (line 48-71) which properly enforces authorization:
```php
public function update(Request $request): JsonResponse
{
$this->userHasPermission(PermissionType::FAQ_EDIT); // Proper permission check
// ... also verifies CSRF token ...
}
```
The `userIsAuthenticated()` method in `AbstractController` (line 258-263) only checks `$this->currentUser->isLoggedIn()`:
```php
protected function userIsAuthenticated(): void
{
if (!$this->currentUser->isLoggedIn()) {
throw new UnauthorizedHttpException(challenge: 'User is not authenticated.');
}
}
```
There is no admin-level middleware in the `Kernel` — it registers only RouterListener, LanguageListener, ControllerContainerListener, and exception listeners. The admin API entry point (`admin/api/index.php`) shares the same bootstrap and session as the frontend, meaning a frontend user's session cookie is valid for admin API requests.
Additionally, this endpoint lacks CSRF token verification (unlike `update()`), though the primary issue is the missing authorization since the attack vector is a logged-in user acting directly.
PoC
```bash
Step 1: Register as a regular user on the phpMyFAQ frontend
(or use any existing non-admin authenticated session)
Step 2: As the authenticated non-admin user, delete tag with ID 1:
curl -X DELETE 'https://target.com/admin/api/content/tags/1' \
-H 'Cookie: PHPSESSID='
Expected: 401 or 403 (user lacks FAQ_EDIT permission)
Actual: 200 OK with {"success": "..."}
Step 3: Enumerate and delete all tags:
for i in $(seq 1 100); do
curl -s -X DELETE "https://target.com/admin/api/content/tags/$i" \
-H 'Cookie: PHPSESSID='
done
```
Impact
Any authenticated user (including regular frontend users who registered through the public registration form) can delete all tags in the phpMyFAQ instance. This results in:
• Data integrity loss: Tags are permanently deleted from the database. All FAQ-to-tag associations are destroyed.
• Disruption of FAQ organization: Tag-based navigation, filtering, and tag clouds become empty or broken.
• No recoverability without backup: Deleted tags and their associations cannot be restored without a database backup.
The impact is limited to tags (not FAQ content itself), but in large installations with extensive tag taxonomies, this could significantly degrade usability.
Recommended Fix
Add the `FAQ_EDIT` permission check and CSRF token verification to `TagController::delete()`, consistent with `TagController::update()`:
```php
[Route(path: 'content/tags/{tagId}', name: 'admin.api.content.tags.id', methods: ['DELETE'])]
public function delete(Request $request): JsonResponse
{
$this->userHasPermission(PermissionType::FAQ_EDIT);
$tagId = (int) Filter::filterVar($request->attributes->get('tagId'), FILTER_VALIDATE_INT);
if ($this->tags->delete($tagId)) {
return $this->json(['success' => Translation::get(key: 'ad_tag_delete_success')], Response::HTTP_OK);
}
return $this->json(['error' => Translation::get(key: 'ad_tag_delete_error')], Response::HTTP_BAD_REQUEST);
}
```
At minimum, add `$this->userHasPermission(PermissionType::FAQ_EDIT)` to enforce the same authorization as the update and search endpoints. Consider also adding a dedicated `TAG_DELETE` permission type for more granular access control.
Related
Weakness · CWE (1)
- CWE-862 Missing Authorization weakness