
Akaunting 3.1.21 - Improper authorization in BulkActions handle dispatch
8.7
High
Discovered by
External researcher
Summary
Full name
Akaunting 3.1.21 - Improper authorization in BulkActions handle dispatch
Code name
State
Public
Release date
Affected product
Akaunting
Vendor
Akaunting
Affected version(s)
3.1.21
Fixed version(s)
3.2.1
Vulnerability name
Privilege escalation
Vulnerability type
Remotely exploitable
Yes
CVSS v4.0 vector string
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS v4.0 base score
8.7
Exploit available
Yes
CVE ID(s)
Description
Akaunting 3.1.21 contains an authenticated improper authorization vulnerability in the common BulkActions dispatcher.
The BulkActions endpoint accepts a request-controlled handle parameter and invokes a method with the same name on the resolved BulkAction class. Authorization is coupled to the $actions metadata array, but the vulnerable dispatcher only checks permissions when the user-supplied handle exists as a direct key in that array.
This design misses two important cases:
internal submit handles, such as
update, declared under a modal action likeedit;callable methods, such as
destroy, that exist on the BulkAction class but are not exposed as direct action keys.
As a result, an authenticated user with access to the admin route group but without user-management permissions can call handle=update or handle=destroy against App\BulkActions\Auth\Users. In the default role model, a Manager has admin-panel access but lacks auth-users permissions. The Manager can still send a direct POST request to the BulkActions endpoint and update user roles or delete other users.
Vulnerability
Root Cause
The BulkActions route is exposed to authenticated admin-panel users
routes/admin.phpregisters the common bulk action endpoint:The route is mounted under
/{company_id}with theadminmiddleware group, which requires authentication andread-admin-panel, but it does not enforce per-resource permissions by itself.The vulnerable dispatcher trusts the request-controlled handle
In Akaunting 3.1.21,
BulkActions::action()readshandlefrom the request:It then checks permission only if the submitted handle is present as a direct key in
$bulk_actions->actions:If the handle is not in
$actions, the condition is false and no permission is enforced.The dispatcher then invokes the handle as a method
The same request-controlled handle is used for dynamic method dispatch:
There is no allowlist check in
3.1.21to ensure the method belongs to a declared action or inherits the permission of a parent modal action.User bulk actions expose privileged methods
App\BulkActions\Auth\Usersdeclares these intended actions:The class also implements
update()anddestroy():In
3.1.21, directhandle=updateandhandle=destroyrequests bypass the intendedupdate-auth-usersanddelete-auth-usersgates.The job layer does not restore the missing permission check
UpdateUsersyncs roles if the request containsroles:DeleteUseronly prevents deleting the current user:Neither job checks whether the caller has
update-auth-usersordelete-auth-users.The fix adds a handle allowlist and modal-handle permission inheritance
Commit
96c38008cf6f907e3e7341d9660de2912e0f2f13adds an allowlist guard toBulkActions::action(). The fixed code resolves direct action keys and modal submit handles before checking permissions. Unknown handles now return a 403-style error instead of being dynamically invoked.
Impact
An authenticated Manager, or any account with read-admin-panel but without user-management permissions, can bypass intended authorization in Akaunting 3.1.21.
Confirmed impact from source review:
Change another user's attributes accepted by
UpdateUser.Sync arbitrary submitted role IDs onto selected users through
handle=update.Promote the attacker-controlled Manager account to Admin if the Admin role ID is known or guessed.
Delete other user accounts through
handle=destroy.Disable users through inherited or explicit update paths where the handle is not correctly permission-gated.
Potential business impact:
Privilege escalation from Manager to Admin.
Unauthorized user administration.
Loss of account availability by deleting or disabling users.
Unauthorized access to financial and administrative data after role escalation.
The issue requires authentication, an active session, and a valid CSRF token. It does not require user interaction from a victim. The attacker also needs a valid {company_id} route context and target user IDs. User IDs are typically low-entropy database identifiers and may be discoverable from responses, logs, predictable sequencing, or prior access.
PoC
Preconditions
Akaunting 3.1.21.
Application reachable at
http://localhost:8080.An authenticated Manager account.
The Manager has
read-admin-panelbut does not haveupdate-auth-usersordelete-auth-users.A valid session cookie and CSRF token for the Manager.
Target user IDs and role IDs are known. In default seeded/local environments, these values are usually small numeric IDs.
Local setup from the original submission
The original CVE submission uses Akaunting's Docker image pinned to 3.1.21-v:
The submission then creates a Manager attacker account through Artisan/Tinker and assigns the default manager role while attaching the user to company 1. The Manager account is used to prove that a user with admin-panel access but without update-auth-users can still call the vulnerable BulkActions endpoint.
When creating the Manager user through Tinker, assign the password as the plaintext value, not bcrypt('Manager1234!'), because App\Models\Auth\User::setPasswordAttribute() hashes the value during assignment. Passing an already-hashed value causes double hashing and prevents login.
Step 1 - Confirm the attacker starts as Manager
Log in as the Manager user and observe that the account does not have user-management permissions. The supplied evidence shows Attacker Manager with the Manager role before exploitation.
Step 2 - Promote the Manager account through the bulk update handle
Send a direct POST request to the BulkActions endpoint using handle=update:
Expected vulnerable result:
BulkActions::action()does not findactions['update']['permission'].The permission condition is skipped.
$bulk_actions->update($request)executes.UpdateUsersyncs the submitted Admin role onto the selected attacker user.The attacker account is upgraded from Manager to Admin.
Step 3 - Delete a target user through the bulk destroy handle
Send a direct POST request using handle=destroy:
Expected vulnerable result:
BulkActions::action()does not findactions['destroy']['permission'].The permission condition is skipped.
$bulk_actions->destroy($request)executes.DeleteUserdeletes the target user unless the target is the current attacker account.
Step 4 - Confirm the impact
Refresh the Users page or inspect the database.
Observed evidence supplied with the report:
Before exploitation,
Attacker Managerhas roleManager.After exploitation,
Attacker Managerhas roleAdmin.The footer in both screenshots identifies Akaunting
Version 3.1.21.
Evidence of Exploitation
Video of exploitation:
Static evidence:

Our security policy
We have reserved the ID CVE-2026-19198 to refer to this issue from now on.
System Information
Akaunting
Version: 3.1.21
Operating System: Any
References
GitHub Repository: https://github.com/akaunting/akaunting
Patch: https://github.com/akaunting/akaunting/releases/tag/3.2.1
Mitigation
An updated version of Akaunting is available on the vendor page.
Credits
The vulnerability was discovered by Jaime Ramirez, an independent security researcher.
Timeline
Vulnerability discovered
Vendor contacted
Follow-up with vendor
Public disclosure
Does your application use this vulnerable software?
During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.














