CVE-2026-16772: Privilege Escalation to Admin in Akaunting via Self-Service Profile Update
| CVE | CVE-2026-16772 |
|---|---|
| CERT/CC | VU#737420 |
| Severity | High CVSS v3.1 base score 8.8 |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Weaknesses | CWE-862 — Missing Authorization • CWE-269 — Improper Privilege Management |
| Product | Akaunting (open-source accounting and invoicing platform) |
| Affected | Akaunting ≤ 3.1.21 |
| Fixed in | No fix available Unpatched at time of publication |
| Reported by | Louis Sanchez — Voke Cyber |
| Coordination | Reported to the vendor 2026-05-20; CERT/CC coordinated (VU#737420) after no response. |
No patch available
At the time of publication there is no fixed release. Operators of self-hosted Akaunting should apply the interim mitigations below and review which accounts currently hold the administrator role. Treat any account on the instance with the default self-service profile permission as able to promote itself to full administrator.
Summary
Akaunting contains a privilege-escalation flaw that lets any low-privileged user promote themselves to full administrator by editing their own profile. When a user saves their profile, the update job reads the list of roles from the submitted form and applies it directly, without ever checking whether that user is allowed to assign roles. A user who holds only the ordinary "update your own profile" permission — which the seeded manager and accountant roles both carry — can add the administrator role to their own account and gain complete control of the installation. There is no exploit or trick involved: the attacker submits their own profile form with an extra role value, and the server obediently makes them an admin.
Background: what Akaunting is and why the blast radius matters
Akaunting is a popular open-source accounting and invoicing platform used by small businesses to manage their finances, often with several staff members who each hold a limited role such as employee, accountant, or manager. The permission system exists precisely so that a limited user cannot do administrator-level things.
Akaunting holds a company's financial records: invoices, bills, customer and vendor details, bank and payment information, and reports. Full administrator access to that data means an attacker can read everything, alter or delete financial records, create and pay bills, manage users, and change company settings.
The important detail is who can do this. It is not an outside attacker and not an existing admin. It is any account with the most basic self-service permission, which almost every user in an Akaunting company has by default. In practice that means any employee, contractor, or limited accountant given a login can quietly make themselves the owner of the whole company's books.
Root cause: a role list applied without an authorization check
When a user updates their own profile, Akaunting dispatches an update job that saves the submitted fields. Among those fields is the set of roles assigned to the user, and the job applies them with a single unconditional call that synchronizes the user's roles to exactly what the form contained.
In app/Jobs/Auth/UpdateUser.php, the relevant part of handle() is:
if ($this->request->has('roles')) {
$this->model->roles()->sync($this->request->get('roles'));
}
The problem is that this role synchronization happens with no authorization check. The job's own authorize() method only blocks a user from disabling themselves and an orphan-company edge case. It never asks whether the person making the change is allowed to manage roles. It simply trusts the role list in the request.
The web controller that dispatches this job, app/Http/Controllers/Auth/Users.php, explicitly permits a user to update their own record:
$user = user_model_class()::find($user_id);
if ((user()->cannot('update-auth-users') && ($user->id != user()->id)) || empty($user)) {
abort(403);
}
A user editing their own record passes through the $user->id == user()->id branch. The action itself is reachable by anyone holding the default update-auth-profile permission, which the seeded manager and accountant roles both carry. So a limited user can include the administrator role ID in the profile-update request and have it applied to their own account.
The boundary of this issue
It is worth being precise about the scope, because it is narrower than it first appears. The JSON API endpoints that manage users are correctly permission-gated — the API user controller inherits update-auth-users and delete-auth-users middleware from its base controller — and are not affected. The flaw is specifically in the web self-update path, where the role assignment is trusted without a check. A broader cross-tenant variant through the API was investigated and ruled out for exactly this reason. This advisory is scoped to the confirmed web self-update privilege escalation.
Exploitation
Prerequisite: any authenticated Akaunting account holding the default update-auth-profile permission (the standard self-service permission). The seeded manager and accountant roles both have it.
The user opens their own profile edit page and submits it with the administrator role added to the roles field. After logging in and taking a CSRF token from any page:
POST /1/auth/profile/42 HTTP/1.1
Host: <akaunting-host>
Cookie: <session>
X-CSRF-TOKEN: <token>
Content-Type: application/x-www-form-urlencoded
_method=PATCH&name=Mallory&email=mallory@example.invalid&landing_page=/dashboard&roles%5B%5D=1
Here 42 is the attacker's own user ID and role ID 1 is the seeded admin role. The server returns 200, the user's role membership is replaced with the administrator role, and reloading any page shows full administrative access: user management, install and updates, module settings, and so on. From that point the user has full administrative control of the company.
No chaining, timing, or interaction from anyone else is required.
Impact
A low-privileged user gains full administrator privileges over the Akaunting installation, and through that:
- Full read access to all financial data — invoices, bills, banking and payment details, customers, vendors, and reports.
- Integrity loss — the ability to create, alter, or delete financial records.
- Persistent control — full user management, including creating further privileged accounts to maintain access.
- Configuration control — company settings including module installation, scheduled jobs, and email/SMTP settings, each of which is a credible route toward broader compromise of the application and potentially the underlying host.
CVSS vector rationale
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — base score 8.8
- AV:N — The Akaunting web application is reachable over the network.
- AC:L — A single profile-update request with one extra field. No special conditions.
- PR:L — Any authenticated account with the default self-service profile permission, which ordinary staff roles hold.
- UI:N — No victim interaction. The attacker drives the exploit entirely from their own session.
- S:U — The escalation happens within the Akaunting authorization boundary; no separate security authority is crossed.
- C:H / I:H / A:H — Administrator access grants full read of all financial data, full modification and deletion of records, and control over settings and users up to and including denial of access to others.
Remediation and interim mitigations
Upgrade to a fixed Akaunting release once available, and track the vendor advisory for the patched version. Until a fix ships:
- Review administrator membership. Audit which accounts currently hold the administrator role and remove any that should not.
- Watch for unexpected role changes. Alert on any profile update that adds a role, particularly the admin role, to an account.
- Limit who is given accounts. Every login on the instance is a potential administrator until this is fixed, so keep the account list as small as the business allows.
The fix
The correct code-level fix is to stop trusting the submitted role list on the self-update path. Role assignment must be gated by an explicit authorization check so that a user cannot change their own roles unless they hold the permission to manage users:
if (user()?->can('update-auth-users')) { if ($this->request->has('roles')) { $this->model->roles()->sync($this->request->get('roles')); } if ($this->request->has('permissions')) { $this->model->permissions()->sync($this->request->get('permissions')); } // keep the existing companies sync behind the same gate }
The rule is simple: the permission to change your own name and email is not the permission to change your own roles. The self-update path conflated the two, and separating them closes the hole.
Disclosure timeline
- 2026-05 — Vulnerability discovered through source review of Akaunting.
- 2026-05-20 — Vendor notified at security@akaunting.com. No response received.
- 2026-06-04 — CERT/CC coordination case opened (VU#737420).
- 2026-07-23 — CVE-2026-16772 reserved by CERT/CC.
- 2026-08-14 — Coordinated public disclosure. No vendor fix available.
References
- CVE record: CVE-2026-16772
- CERT/CC vulnerability note: VU#737420
- Akaunting project: akaunting.com • github.com/akaunting/akaunting
- Blog post (story and disclosure narrative): The Profile Page That Handed Out Admin: CVE-2026-16772 in Akaunting
Found and reported by Louis Sanchez, Founder & Principal Security Consultant at Voke Cyber (OSCP, OSWA, CISSP, CCSK). Prior advisories: CVE-2026-17613 (Penpot), CVE-2026-16751 (Ente), CVE-2026-16624 (Cal.com), CVE-2026-15630 (Casdoor), CVE-2026-39878 (Chamilo LMS), CVE-2026-48742 (Coolify), CVE-2026-35198 (HeyForm), CVE-2026-48507 (Snipe-IT), and CVE-2026-42318 (GLPI).
We find the bugs tools miss
A profile form that quietly accepts a roles field does not show up in a dependency scan. It surfaces when someone reads the update job and asks which of these submitted fields anyone actually checked the caller was allowed to set. That is how we approach web application and API testing for clients across the Charlotte, NC area and nationwide.
Get a Quote