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)

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

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:

($event != 'custom') ? form.payment_terms = $event : form.payment_terms = <stored value>;
($event != 'custom') ? form.payment_terms = $event : form.payment_terms = <stored value>;
($event != 'custom') ? form.payment_terms = $event : form.payment_terms = <stored value>;
($event != 'custom') ? form.payment_terms = $event : form.payment_terms = <stored value>;

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

  1. A Manager can read and update settings-invoice; the built-in role seed grants settings-invoice => 'r,u' (database/seeds/Permissions.php:83-117).

  2. PATCH /{company_id}/settings/invoice is registered as settings.invoice.update (routes/admin.php:191-195). SettingController::update() stores each submitted field, including payment_terms, under the request prefix (app/Abstracts/Http/SettingController.php:31-132).

  3. The Invoice Settings page supplies setting('invoice.payment_terms') to <x-form.group.payment-term> (resources/views/settings/invoice/edit.blade.php:21).

  4. When that saved value is not a predefined option, PaymentTerm::render() copies it to $this->value and changes the selected option to custom (app/View/Components/Form/Group/PaymentTerm.php:20-40).

  5. The component injects $value directly into all three @interface variants (resources/views/components/form/group/payment_term.blade.php:123-129). The no-v-model branch 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

  1. 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 has settings-invoice read/update permission; it does not need Administrator permission to store the setting.

  2. 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 custom payment_terms input; selecting the option alone does not persist a value.

  3. Enter the following harmless payload in the custom payment_terms input:

    form.constructor.constructor(String.fromCharCode(100,111,99,117,109,101,110,116,46,116,105,116,108,101,61,39,88,83,83,95,80,65,89,77,69,78,84,95,84,69,82,77,95,69,88,69,67,85,84,69,68,39))()
    form.constructor.constructor(String.fromCharCode(100,111,99,117,109,101,110,116,46,116,105,116,108,101,61,39,88,83,83,95,80,65,89,77,69,78,84,95,84,69,82,77,95,69,88,69,67,85,84,69,68,39))()
    form.constructor.constructor(String.fromCharCode(100,111,99,117,109,101,110,116,46,116,105,116,108,101,61,39,88,83,83,95,80,65,89,77,69,78,84,95,84,69,82,77,95,69,88,69,67,85,84,69,68,39))()
    form.constructor.constructor(String.fromCharCode(100,111,99,117,109,101,110,116,46,116,105,116,108,101,61,39,88,83,83,95,80,65,89,77,69,78,84,95,84,69,82,77,95,69,88,69,67,85,84,69,68,39))()

    The String.fromCharCode() argument decodes to document.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 to document.title.

  4. Click Save. The form issues the normal PATCH /1/settings/invoice request. Confirm the normal success response, then sign out. SettingController::update() stores the submitted field as the company-scoped invoice.payment_terms value.

  5. In a separate browser profile, sign in as the Administrator for company 1 and navigate directly to /1/settings/invoice. Do not change the selector or submit the form.

  6. Verify that the browser title becomes XSS_PAYMENT_TERM_EXECUTED. This confirms that the non-predefined stored value was loaded into PaymentTerm::$value, rendered into the component's @interface expression, 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.

Disclosure policy

System information

  • Product: Akaunting

  • Version: 3.3.0

  • Operating System: Any supported Akaunting deployment

References

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.