
Akaunting 3.3.0 - Stored XSS in custom invoice payment terms
7.2
High
Detected by

Fluid Attacks AI SAST Scanner
Disclosed by
Miguel Gómez
Abstract
Fluid Attacks' AI SAST detected CVE-2026-118597, a high-severity stored cross-site scripting vulnerability (CWE-79) in Akaunting 3.3.0. A Manager can execute JavaScript in an Administrator session through custom invoice payment terms. It has a CVSS v4.0 base score of 7.2. This advisory includes a proof of concept.
Summary
Full name
Akaunting 3.3.0 - Stored XSS through custom invoice payment terms enables cross-user JavaScript execution
Code name
State
Public
Release date
Affected product
Akaunting
Vendor
Akaunting
Affected version(s)
3.3.0
Vulnerability name
Stored cross-site scripting (XSS)
Vulnerability type
Remotely exploitable
Yes
CVSS v4.0 vector string
CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:H/VI:H/VA:N/SC:L/SI:L/SA:N
CVSS v4.0 base score
7.2
Exploit available
Yes
CVE ID(s)
CWE
CAPEC
Description
Akaunting 3.3.0 contains a stored cross-site scripting vulnerability in the custom invoice payment-terms setting. An authenticated user with the built-in Manager role can save a crafted custom payment-term value. When another authenticated user in the same company, including an Administrator, opens Settings → Invoice, Akaunting embeds that stored value into an Alpine/Vue event-handler expression and the browser evaluates it as JavaScript.
Vulnerability
Root cause
PaymentTerm::render() treats every selected value that is not one of the fixed numeric options as a custom payment-term value. It assigns the saved setting to $this->value and selects custom. The Invoice Settings view passes the company-level invoice.payment_terms setting to this component.
The component template subsequently interpolates $value in @interface, an HTML attribute that becomes client-side JavaScript. In the normal Invoice Settings use, the rendered expression is equivalent to:
Blade's default HTML escaping is not a JavaScript serializer. It prevents markup delimiters such as < and > from forming HTML, but it leaves a JavaScript expression composed of identifiers, dots, parentheses, commas, and digits executable. For example, the expression-style payload used in the supplied test remains syntactically executable after htmlspecialchars(..., ENT_QUOTES).
Custom payment terms are expected to be text or a number of days. There is no product requirement for them to be executable JavaScript.
Source-to-sink path
A Manager can read and update
settings-invoice; the built-in role seed grantssettings-invoice => 'r,u'(database/seeds/Permissions.php:83-117).PATCH /{company_id}/settings/invoiceis registered assettings.invoice.update(routes/admin.php:191-195).SettingController::update()stores each submitted field, includingpayment_terms, under the request prefix (app/Abstracts/Http/SettingController.php:31-132).The Invoice Settings page supplies
setting('invoice.payment_terms')to<x-form.group.payment-term>(resources/views/settings/invoice/edit.blade.php:21).When that saved value is not a predefined option,
PaymentTerm::render()copies it to$this->valueand changes the selected option tocustom(app/View/Components/Form/Group/PaymentTerm.php:20-40).The component injects
$valuedirectly into all three@interfacevariants (resources/views/components/form/group/payment_term.blade.php:123-129). The no-v-modelbranch is used by Invoice Settings. The event-handler compiler evaluates the resulting expression when the custom selection is processed.
Impact
An authenticated Manager can persist script-capable content that executes whenever a more privileged user visits Invoice Settings for the same company. JavaScript in an Administrator's session can read application data exposed to that session and invoke authenticated application actions, including user-management actions.
PoC
Preconditions
Use an Akaunting 3.3.0 deployment with its first company (ID 1), an account with the predefined Manager role, and a separate Administrator account in that company. The Manager must be able to read and update Invoice Settings; the Administrator must be able to access Invoice Settings. Use separate browser profiles for the two accounts.
Step-by-step
Deploy Akaunting 3.3.0 and complete its initial setup. Work in company
1. Create an Administrator account and a separate user with the built-in Manager role in that company. The Manager role hassettings-invoiceread/update permission; it does not need Administrator permission to store the setting.In a dedicated browser profile, sign in as the Manager and open Settings → Invoice at
/1/settings/invoice. Under Payment Terms, select Custom days. This exposes the custompayment_termsinput; selecting the option alone does not persist a value.Enter the following harmless payload in the custom
payment_termsinput:The
String.fromCharCode()argument decodes todocument.title='XSS_PAYMENT_TERM_EXECUTED';form.constructor.constructor()evaluates that string in the affected client-side handler. The payload has no effect other than assigning the fixed marker todocument.title.Click Save. The form issues the normal
PATCH /1/settings/invoicerequest. Confirm the normal success response, then sign out.SettingController::update()stores the submitted field as the company-scopedinvoice.payment_termsvalue.In a separate browser profile, sign in as the Administrator for company
1and navigate directly to/1/settings/invoice. Do not change the selector or submit the form.Verify that the browser title becomes
XSS_PAYMENT_TERM_EXECUTED. This confirms that the non-predefined stored value was loaded intoPaymentTerm::$value, rendered into the component's@interfaceexpression, and evaluated in the Administrator's authenticated origin.
Evidence of Exploitation
Video of exploitation:
Static evidence:

Our security policy
We have reserved the ID CVE-2026-118597 to refer to this issue from now on.
System information
Product: Akaunting
Version: 3.3.0
Operating System: Any supported Akaunting deployment
References
GitHub Repository: https://github.com/akaunting/akaunting
Security: https://github.com/akaunting/akaunting/blob/master/SECURITY.md
Release: https://github.com/akaunting/akaunting/releases#release-3.3.0
Solution and Workaround
Solution
Do not interpolate the value into JavaScript source. Emit it as data using a JavaScript-safe encoder such as Laravel's @js($value)/Js::from($value), and assign it as a string, or bind the value through the framework's data model. The server should also validate invoice.payment_terms as a bounded integer if custom payment terms are intended only to represent a number of days.
Workaround and temporary mitigation
Until a fixed release is available, replace custom invoice.payment_terms values with predefined numeric options and restrict settings-invoice update access to trusted users. These measures reduce exposure but do not correct the unsafe sink.
Credits
The vulnerability was detected by AI SAST Scanner and disclosed by Miguel Gómez from Fluid Attacks' Offensive Team.
Timeline
Vulnerability discovered
Vendor contacted
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.














