Reg. No. 40203410806VAT LV40203410806

Theme

Book a consultationBook

02 / 10Vulnerability disclosure

SQL injection in the Zalktis accounting app via e-invoice imports

Zalktis accounting software (Windows)

CVE-2026-59109 is a SQL injection OffSeq found in Zalktis, a Latvian accounting application. When Zalktis imports a received electronic invoice, it builds its database lookups by pasting fields from the incoming document — the registration and VAT numbers, the EAN barcode and the unit-of-measure code — straight into the SQL text, with no parameterisation and no escaping. Several of those fields are set by whoever sends the invoice, so a single crafted invoice that a bookkeeper simply imports can read and alter the accounting database. Zalktis Programmas fixed it by parameterising the queries.

Severity: HighCVSS8.7CVE-2026-591095 min read

§ 01. Overview

Zalktis is a Windows accounting application used by Latvian businesses. Like most accounting software in Latvia, it imports electronic invoices — UBL documents delivered over the national e-address / PEPPOL network — and e-commerce exports, turning each into ledger entries. Importing a received invoice is an everyday bookkeeping action.

OffSeq found that Zalktis built the SQL behind those imports by pasting fields taken straight from the incoming document into the query text. Several of those fields are controlled by whoever sends the invoice, so a crafted invoice can break out of a string literal and run attacker-chosen SQL inside the accounting database. No account, prior relationship or network position is needed — on the PEPPOL network anyone can address an invoice to the victim, and the only step the victim takes is importing it. Zalktis Programmas fixed the issue by parameterising the queries; it is tracked as CVE-2026-59109, coordinated by CERT.LV.

§ 02. Importing an e-invoice

When Zalktis imports a received e-invoice or an e-commerce export, it reads the document (ReadXml / XmlSerializer.Deserialize) and, for each party and line item, checks whether the referenced record already exists — a customer by registration or VAT number, a product by barcode, a unit of measure by its code. Those checks are validation routines such as KlientsNMREksiste, KlientsPVNnrEksiste and PreceEANEksiste.

Each routine issues a SELECT against the local VistaDB database. The problem is how the SELECT is built: the value being looked up is taken from the incoming document and concatenated directly into the SQL string, and the query runs on an unparameterised OleDbCommand / VistaDBDataAdapter. The application ships an escaping helper, Dazadi.sql_txt(), that strips single quotes — but it is never called on these import paths.

§ 03. Root cause: partner-controlled fields in the query text

Four fields carried from the incoming document reach a query unescaped:

Field in the received documentQuery builtLocation
Registration number (BuyerParty/SellerParty RegNumber)SELECT PersonalID FROM Personal WHERE NMRNR = '<field>'DatuParbaude.cs:320
VAT number (VATRegNumber)... WHERE PvnNr = '<field>'DatuParbaude.cs:459
EAN / barcode... WHERE Svitrkods = '<field>'DatuParbaude.cs:763
Unit-of-measure code (<cbc:InvoicedQuantity unitCode="…">)SELECT MervienibaLV FROM Units WHERE UneceCode = '<field>'frmreksaraksti.cs:3381
Table 1The four partner-controlled fields concatenated into query text.

Each sits inside a '…' string literal in the SQL. Because the sender controls the field and Dazadi.sql_txt() is not applied, a single quote in the field ends the literal and everything after it is parsed as SQL. The registration and VAT numbers and the barcode are attacker-chosen values in the invoice; the unit-of-measure code is an XML attribute on every line, so the unitCode path is reached automatically for each line of the invoice during import, with no extra action.

§ 04. Proof of concept

The finding was demonstrated with a non-destructive, boolean-based test that changes no data.

A valid UBL invoice carries a payload in the buyer's registration-number field:

xml

<cac:AccountingCustomerParty>
  <cac:Party>
    <cac:PartyLegalEntity>
      <cbc:CompanyID>40003' AND '1'='1</cbc:CompanyID>
    </cac:PartyLegalEntity>
  </cac:Party>
</cac:AccountingCustomerParty>

When the invoice is imported, KlientsNMREksiste builds and runs:

sql

SELECT PersonalID FROM Personal WHERE NMRNR = '40003' AND '1'='1'

The '1'='1' variant and the '1'='2 variant return different results — record found versus not found — which confirms the field is evaluated as SQL rather than treated as data. From the same entry point, a UNION extracts data:

sql

SELECT PersonalID FROM Personal WHERE NMRNR = '40003' UNION SELECT Parole FROM Lietotajs WHERE '1'='1'

The unit-of-measure code reaches the database with no user targeting at all — it fires for every invoice line on import:

xml

<cbc:InvoicedQuantity unitCode="C62' UNION SELECT MervienibaLV FROM Units WHERE '1'='1">1</cbc:InvoicedQuantity>

which produces:

sql

SELECT MervienibaLV FROM Units WHERE UneceCode = 'C62' UNION SELECT MervienibaLV FROM Units WHERE '1'='1'

All testing was carried out in a controlled environment against systems OffSeq controls; no third-party systems were touched.

§ 05. Impact and severity

The published metrics are CVSS 4.0 base 8.7 (High) (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) and CVSS 3.1 base 8.8 (High) (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H). The vector is Network, low-complexity, no privileges: on the Latvian e-address / PEPPOL network any sender can deliver an invoice to the victim. The only interaction (UI:P / UI:R) is the victim importing it — a routine bookkeeping operation, not a security decision.

Successful injection gives full access to the accounting database: confidential data can be read and exfiltrated with UNION- and boolean-based extraction — customer records, credentials such as the Lietotajs.Parole values in the example, financial data — and records and payments can be altered. Because the vulnerable pattern is shared across roughly 20 import channels, it is not one isolated form: every installation that imports documents is affected.

§ 06. Remediation

The fix is to stop building queries from document text. The affected SELECTs must use parameters — bound values the database treats as data, never as SQL — instead of string concatenation, on every one of the shared import paths: the registration- and VAT-number and barcode lookups, the unit-of-measure lookup, and the roughly 20 channels that reuse the pattern.

Zalktis Programmas parameterised the affected queries and released fixed builds: 2026.1.586 for the branch preceding 1 July 2026, and 2026.2.592 for the branch released from 1 July 2026 onward. Users should upgrade to one of these builds or later. Until a fixed build is deployed, avoid importing e-invoices and e-commerce exports received from untrusted senders, and keep restorable backups of the accounting database taken before any import.

§ 07. Disclosure and outcome

The vulnerability was reported to CERT.LV through Latvia's national coordinated-vulnerability-disclosure platform on 30 June 2026, triaged and forwarded to the vendor on 1 July. Zalktis Programmas confirmed remediation by query parameterisation on 16 July 2026, in builds 2026.1.586 and 2026.2.592, and CVE-2026-59109 was reserved on 30 July 2026. The disclosure is coordinated by CERT.LV.

The discovery is credited to Nils Putniņš (OffSeq Cybersecurity).

§ 08. Disclosure timeline

  1. 2026-06-30OffSeq reports the SQL injection to CERT.LV through the national coordinated-disclosure platform
  2. 2026-07-01CERT.LV triages the report and forwards it to the vendor
  3. 2026-07-16Zalktis confirms the fix — the affected queries are parameterised — in builds 2026.1.586 and 2026.2.592
  4. 2026-07-30CVE-2026-59109 reserved

Have similar exposure in your environment?