CVE-2026-111888 and CVE-2026-111889: NEWS_DELETE to edit, nothing at all to read the dashboard
phpMyFAQ guards its admin area one right at a time: each route names the permission it needs on its first line. Reading those first lines on the 4.1 branch, I found one that named the wrong right, so a moderator allowed to delete news could rewrite it, and one that named none, so any logged-in member could open the admin dashboard and read unpublished questions and the newest accounts. Both were fixed the day I reported them.
Disclaimer. This is personal security research, carried out on my own equipment against a public open-source project. It has no connection to any current or former employer, and no client, employer or production environment was involved at any point. Every request in it went to fresh phpMyFAQ installs on my own machine, set up through the project’s own installer, with test accounts I created for the purpose. Both issues were reported privately through the project’s security advisory process. They are written up here after the advisories, the fixed release and both CVE records were public.
phpMyFAQ has a fine-grained admin area. Instead of one “administrator” switch, an account holds individual rights: addnews, editnews and delnews for news, separate rights for FAQs, users, statistics, backups and configuration. A site can make someone a news moderator without letting them touch anything else.
In the code, each of those rights is enforced the same way. The first line of an admin controller method names the right it needs:
$this->userHasPermission(PermissionType::NEWS_ADD);
If the account doesn’t hold it, the call throws and the request gets a 403. That makes the whole permission model easy to audit: read the first line of every method and ask whether it names the right thing. In September I did that for the admin controllers of the 4.1 branch, at release 4.1.8. Two first lines were wrong, in two different ways.
Line 122: the right next door
Api/NewsController.php has four write actions, and three of them name the right you’d expect: create asks for NEWS_ADD, delete for NEWS_DELETE, activate for NEWS_EDIT. The fourth is the edit itself:
// phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/NewsController.php, 4.1.8
#[Route(path: 'admin/api/news/update')]
public function update(Request $request): JsonResponse
{
$this->userHasPermission(PermissionType::NEWS_DELETE); // line 122
So an account with delnews could rewrite any news item’s header, body, author, link and active flag, whether or not it held editnews. In practice the account also needs addnews, but only for a CSRF token: the token the update checks is issued on every news page, including the “add” page, and it sits in the moderator’s own cookie jar.
I ran it on 4.1.8 with an account holding addnews and delnews, and no editnews. The update returned 200. The rewritten header and body then appeared on the public home page and in the public REST API, both read back without logging in, and in the database row. Two controls pinned the cause to this one line. The same account was refused news/activate and the edit page with a message naming NEWS_EDIT, so it really lacked the right. And an account holding only editnews was refused the update, so the guard really was NEWS_DELETE.
The impact is small, and I scored it that way: CVSS 3.1 PR:H/I:L, 2.7 Low. The attacker is already a trusted news moderator, and what they gain is the one news right they were not given.
A sentence in another researcher’s advisory
What made this one interesting was where the right answer was already written down.
On main, the development branch, the line has read NEWS_EDIT since 10 April. It was fixed in 9d06e8752, a commit titled “corrected and improved permission checks” that touches twenty-eight files. The change never reached the 4.1 branch, and every 4.1 release through 4.1.8 shipped the old line.
In August, phpMyFAQ published GHSA-6w97-49h8-58wh, another researcher’s report about admin pages that checked the wrong permission constant. To show which right those pages should have asked for, that advisory points at the news API:
“The corresponding mutation APIs enforce the correct permissions (
Api/NewsController.phprequiresNEWS_EDIT), confirming the intended privilege level.”
That sentence is true on main, where that research was done. On the stable branch it is false. The file it cites as the reference is the one with the same mistake.
I quoted the sentence in my report. It settled two questions at once: this was not a duplicate of GHSA-6w97, and the intended right was NEWS_EDIT, as both the maintainer and the other researcher had already agreed in writing.
DashboardController::index: no right at all
The second report is about the first page you see after logging into the admin area. In 4.1.8 the dashboard’s first line is a different call:
#[Route(path: '/', name: 'admin.dashboard', methods: ['GET'])]
public function index(Request $request): Response
{
$this->userIsAuthenticated();
userIsAuthenticated() asks whether there is a session, not what the session may see. As far as I found, it was the only route in the admin namespace that stopped there. The page then rendered:
- every unpublished FAQ question, with a link to its admin edit page, from a bare
SELECT id, lang, thema FROM faqdata WHERE active = 'no'; - the five newest accounts: user ID, display name and join date;
- site-wide counters: visits, users online, and the number of FAQs, comments, questions and users.
Every one of those is protected somewhere else. The FAQ list needs the FAQ rights, the user list needs the user rights, and the dashboard’s own statistics API needs STATISTICS_VIEWLOGS. Two of those dashboard API actions had been fixed in August, in GHSA-gpwm-82cg-4wfw. The page that displays all of that data had no check of its own.
The account in my test held nothing. It had no admin rights at all and was not a super admin, which matters because super admins skip the permission check entirely. It logged in through the ordinary front-end form, never touched the admin interface, and then requested /admin/index.php/. The response contained the question text of a draft created by the real administrator, a record that returned 404 on every public path, along with the names and join dates of accounts the test account had no way to list. In the same session, every sibling route refused it with a message naming the right it lacked. Anonymous requests to the dashboard were redirected to the login page.
That puts the boundary exactly at logged in, with no admin rights. It is not anyone: on 4.1.8 a self-registered account starts out blocked, so the attacker is an ordinary activated member. The score was CVSS 3.1 PR:L/C:L, 4.3 Medium. Confidentiality is Low, not High, because only titles, links and identities leak, not the draft answers.
I ran this on 4.1.8 and on main. Both behaved the same way.
Why the dashboard couldn’t simply ask for a right
The fix for the news line is the one-line change you would expect. The dashboard’s fix is more interesting, because its comment explains why the page was open in the first place:
/**
* The dashboard is the landing page of every authenticated user, so it must not require a
* specific right itself. Each data set is therefore gated on the right its dedicated admin page
* already requires, and the underlying queries are only executed when that right is held:
The page can’t require a right, because every account lands there, including one whose only right is to edit news. So the check has to sit inside the page, one widget at a time. bfa5604 adds a getPermissionGatedWidgets() method:
- unpublished FAQs require
FAQ_EDIT; - the newest accounts require
USER_EDIT; - the counters require
STATISTICS_VIEWLOGS; - the backup notice requires
BACKUP.
Each query runs only inside its own branch, so data the user may not see is never fetched, not merely hidden by the template. An account holding none of the four gets an empty dashboard.
That is better than what I suggested. My report proposed gating each widget, but I was thinking of the template. Moving the queries behind the checks means a future template change can’t leak data the controller never loaded. I said so in the thread after reading the commit.
Same morning, same afternoon
I reported both on 17 September. Thorsten Rinne accepted the dashboard report at 16:51 with bfa5604, which also carries 311 lines of new tests. Thorsten accepted the news report at 16:55, and 4eb14b4 swaps NEWS_DELETE for NEWS_EDIT and adds 186 lines of tests for the news API, which had none before. Both fixes are in 4.1.9, tagged on 3 October, and the dashboard fix is also in 4.2.0-beta. Both advisories were published the same morning, crediting me as reporter.
Neither got a CVE through GitHub. Thorsten requested one for the dashboard advisory on 17 September, after I asked in the thread, and again on publishing it. The news advisory never had a request. Two weeks after publication, on 17 October, VulnCheck assigned identifiers to both on its own: I had not submitted either one. CVE-2026-111888 covers the news line and CVE-2026-111889 the dashboard, and both records credit me as reporter.
Where 4.0 fits
The two records don’t agree with the advisories on one point, and I checked it rather than pick a side.
CVE-2026-111889 matches its advisory: 4.1.0 up to 4.1.9, and the 4.2.0 alpha releases up to 4.2.0-beta. That is where I ran it. The recent-users widget didn’t exist before 4.1.0. The unpublished-FAQ widget does appear behind the same login-only entry in 4.0.19, but I didn’t exercise it and didn’t claim it.
CVE-2026-111888 is wider than its advisory. The advisory says >= 4.1.0; the record says 4.0.0 before 4.1.9. The source sides with the record. In both 4.0.0 (December 2024) and 4.0.19, the last 4.0 release, update() asks for NEWS_DELETE. The line came in with the April 2024 refactor that created the controller, so it shipped in every release from 4.0.0 to 4.1.8. I read the 4.0 code but didn’t run it. No 4.0 release carries the fix, so on 4.0 the answer is to upgrade to 4.1.9 or later.
Advisory GHSA-287v-6c6j-x9f7 · CVE CVE-2026-111888 · CVSS 2.7 Low (v3.1), 5.1 Medium (v4.0) · Fix 4eb14b4 · Affected >= 4.1.0, <= 4.1.8 per the advisory, 4.0.0 onward per the record · Fixed 4.1.9
Advisory GHSA-4378-pcwq-fc29 · CVE CVE-2026-111889 · CVSS 4.3 Medium (v3.1), 5.3 Medium (v4.0) · Fix bfa5604 · Affected >= 4.1.0, <= 4.1.8, 4.2.0-alpha · Fixed 4.1.9, 4.2.0-beta
Both reported 17 September · both accepted and fixed the same day · 4.1.9 tagged and both advisories published 3 October, crediting me as reporter · both CVE records published by VulnCheck 17 October.
Eurico Nicacio — @h3llh0und