
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.0
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 common dispatcher treats the request-controlled handle as both an authorization lookup key and a method name. When handle is absent from $bulk_actions->actions, the permission predicate is skipped, but dynamic dispatch still invokes the corresponding public method. The jobs called by update() and destroy() enforce only self-protection rules; they do not restore the missing update-auth-users or delete-auth-users authorization check.
Source-to-sink path
Entry point — the common BulkActions route admits admin-panel users
routes/admin.phpregisters the common bulk action endpoint:The route is mounted under
/{company_id}with theadminmiddleware group. It authenticates the caller and requiresread-admin-panel, but it has no per-resource authorization at this boundary. A Manager can therefore reach the dispatcher even though that role lacks permission to administer users.Attacker-controlled source —
handleselects both the authorization lookup and the operation. In Akaunting 3.1.21,BulkActions::action()readshandlefrom the request:The controller uses that untrusted value to locate a permission, but only if it is a direct key in
$bulk_actions->actions:This is the authorization gap: if the supplied handle is absent from
$actions,isset(...)is false and the entire permission check is skipped. The value is not rejected or normalized at this point.Authorization bypass to sink — the unchecked handle is dynamically invoked. The same request-controlled handle is used for dynamic method dispatch:
The controller then uses the same unchecked value as a method name. In 3.1.21, there is no allowlist to ensure that the method is a declared action or to inherit a parent modal action's permission. Thus a value that skipped step 2 reaches this dynamic-call sink.
Reachable privileged operations — declared actions and their internal handles do not align.
App\BulkActions\Auth\Usersdeclares these intended actions:updateis intentionally the submit handle for the permission-protectededitaction.destroyis a public callable method, but neitherupdatenordestroyis a direct$actionskey. Both are nevertheless callable by the sink in step 3:A direct
handle=updatebypasses the permission assigned toedit(update-auth-users);handle=destroybypasses the gate associated withdelete(delete-auth-users).Privileged effect — the job layer does not restore the skipped authorization.
UpdateUsersyncs roles if the request containsroles:DeleteUseronly prevents deleting the current user:These checks protect only the current account from deletion and certain self-modification cases. Neither job verifies that the caller holds
update-auth-usersordelete-auth-users, so the attacker-controlled role list reachesroles()->sync(...)and the selected target reachesDeleteUser.Verified remediation — 3.2.0 restores the authorization link. Commit
96c38008cf6f907e3e7341d9660de2912e0f2f13, present in 3.2.0 and later releases, adds an allowlist guard toBulkActions::action(). It resolves direct action keys and modal submit handles before authorization, soupdateinheritsedit's permission. 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 update-auth-users or delete-auth-users, can bypass intended user-management authorization.
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 Manager session, CSRF token,
{company_id}, attacker user ID, and administrator role ID.A controlled local test environment with a disposable target account.
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
Product: Akaunting
Version: 3.1.21
Operating System: Any
References
GitHub repository: https://github.com/akaunting/akaunting
Patch: https://github.com/akaunting/akaunting/commit/96c38008cf6f907e3e7341d9660de2912e0f2f13
Mitigation
Upgrade to Akaunting 3.2.0 or a later supported release. It adds an allowlist for direct action keys and modal submit handles, maps
updateback to its parenteditaction, applies that action's permission, and rejects unknown handles with a 403 response.Until upgrading, restrict
read-admin-panelto trusted administrators, review role assignments and user deletions, and invalidate sessions for accounts suspected of exploitation.Preserve the server-side allowlist: never dynamically invoke a request-controlled method name. Resolve handles only from declared metadata and enforce authorization on every state-changing job as defense in depth.
Credits
The vulnerability was discovered by Jaime Ramirez, an independent security researcher.
Timeline
Vulnerability discovered
Vendor contacted
Vulnerability patched
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.














