Akaunting 3.3.0 - Stored XSS through imported invoice numbers

7

High

Detected by

Fluid Attacks AI SAST Scanner

Disclosed by

Miguel Gómez

Abstract

Fluid Attacks' AI SAST detected CVE-2026-119275, a high-severity stored cross-site scripting vulnerability (CWE-79) in Akaunting 3.3.0. An authenticated Manager can execute JavaScript in a recurring-invoice reader's browser. It has a CVSS v4.0 base score of 8.3. Fluid Attacks assigned this CVE as a CNA, and this advisory includes a proof of concept.

Summary

Full name

Akaunting 3.3.0 - Stored XSS through imported recurring-invoice children

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:A/VC:H/VI:H/VA:N/SC:L/SI:L/SA:N

CVSS v4.0 base score

7.0

Exploit available

Yes

Description

Akaunting 3.3.0 contains a stored XSS vulnerability in the XLSX invoice-import workflow. A Manager can import an ordinary invoice whose invoice_number contains HTML, associate it with a recurring invoice, and cause that value to be rendered as HTML when a victim opens the recurring invoice's child section.

Vulnerability

Root cause

Imports\\Sales\\Invoices\\Sheets\\Invoices::map() casts the spreadsheet invoice_number to a string and assigns it to document_number. The import-specific request rule requires a string but does not neutralize HTML. The importer also accepts a supplied parent_id when parent_number is nonempty, allowing an imported ordinary invoice to be linked to an attacker-created recurring invoice.

resources/views/components/documents/show/children.blade.php then builds an anchor as a PHP string with $child->document_number directly inside it. That string is passed as the url placeholder to the recurring.child translation and emitted with Blade's raw {!! !!} syntax. The normal {{ }} escaping is bypassed while constructing the anchor and while rendering the translation, so browser parsing interprets markup stored in the invoice number.

Source-to-sink path

  1. The normal-invoice import route is registered in routes/admin.php; Sales\\Invoices::import() invokes the invoice importer for POST /{company}/sales/invoices/import.

  2. Invoices::map() casts invoice_number, maps it to document_number, and sets parent_id from the import row (app/Imports/Sales/Invoices/Sheets/Invoices.php:33-57). Its validation is derived from Document and accepts required|string|unique rather than neutralizing HTML (app/Http/Requests/Document/Document.php:48-68).

  3. App\\Traits\\Import::getParentId() retains a provided parent ID when parent_number is nonempty (app/Traits/Import.php:200-220), creating the parent-child relationship.

  4. A victim opens the associated recurring invoice. The child view concatenates document_number inside an anchor (resources/views/components/documents/show/children.blade.php:15-19) and raw-renders the translated entry (:21-23). This is the executable HTML sink.

Impact

When an authenticated victim opens the affected recurring-invoice detail page, attacker-controlled JavaScript executes in that victim's authenticated Akaunting session. It can read data exposed to the browser and submit same-origin actions that the victim is authorized to perform. The impact is session compromise within the victim's existing permissions: it is greatest for an administrator and does not independently grant privileges to a lower-privileged victim.

PoC

Preconditions

Use Akaunting 3.3.0, company ID 1, a predefined Manager account, and a separate victim allowed to read recurring invoices. This public proof uses an ordinary invoice-import workbook created from the standard template; it contains no private companion material and no state-changing payload.

Step-by-step

  1. Install the released 3.3.0 version, complete normal setup, create the first company (ID 1), and retain the initial administrator for verification and cleanup.

  2. Create or invite a user in that company with the predefined Manager role only. Complete the invitation flow and sign in as that Manager; no administrator permission, custom role, API token, cache change, or server-side command is part of the proof.

  3. Confirm that Importer, Sales → Invoices, and Sales → Recurring Invoices are available. The stock role has common-import: c and sales-invoices: c,r,u,d, but no user-management permission.

  4. Create or import a recurring invoice parent through Importer → Import Recurring Invoices. Open it under Sales → Recurring Invoices and record its numeric ID from /1/sales/recurring-invoices/<parent-id>.

  5. Create an ordinary-invoice XLSX using the standard invoice-import template. In its invoices sheet, set parent_id to the ID from step 4 and keep parent_number nonempty. Put this exact harmless value in the invoice_number cell:

    <img src=x onerror="document.documentElement.dataset.akauntingXss='SODA_RECURRING_CHILD_XSS'">
    <img src=x onerror="document.documentElement.dataset.akauntingXss='SODA_RECURRING_CHILD_XSS'">
    <img src=x onerror="document.documentElement.dataset.akauntingXss='SODA_RECURRING_CHILD_XSS'">
    <img src=x onerror="document.documentElement.dataset.akauntingXss='SODA_RECURRING_CHILD_XSS'">
  6. Import the workbook through Importer → Import Invoices (POST /1/sales/invoices/import), not the recurring-invoice import page. Confirm that the ordinary child invoice exists and is linked to the recurring parent.

  7. In a separate victim session, open the recurring parent and its child-invoice section.

  8. In browser developer tools, evaluate:

    document.documentElement.dataset.akauntingXss
    document.documentElement.dataset.akauntingXss
    document.documentElement.dataset.akauntingXss
    document.documentElement.dataset.akauntingXss

    The value is SODA_RECURRING_CHILD_XSS. It is set during page rendering; no child-link click, request, or state-changing action is involved.

Evidence of Exploitation

  • Video of exploitation:

  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-119275 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

Escape document_number when constructing the child link and avoid passing a prebuilt HTML string to a raw translation sink. In addition, reject or normalize markup in document numbers on every write path, including XLSX imports, and add regression coverage for imported child invoices rendered by recurring invoices.

Workaround and temporary mitigation

Restrict invoice-import permission to trusted administrators, inspect existing document numbers for markup, and avoid opening recurring invoices with children imported from untrusted workbooks. These measures reduce exposure but do not remove the vulnerable 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.