Security¶
Auditfiles are untrusted input. They arrive from clients, from third-party accounting software and by e-mail, and pyxaf is often run on servers that process files from many sources. pyxaf is designed to read a hostile file safely: it must not read local files, open network connections, or exhaust memory or CPU because of what a file contains.
To report a vulnerability, follow the security policy: privately, through GitHub's private vulnerability reporting or by e-mail, never in a public issue.
Threat model¶
| Threat | Mitigation |
|---|---|
| Entity expansion ("billion laughs", quadratic blow-up) | DOCTYPE and ENTITY declarations are refused before any expansion happens |
| External entities (XXE): reading local files, server-side requests | refused with the DOCTYPE; Expat never opens files or network connections; parameter-entity parsing is disabled |
| Deeply nested elements | max_depth (default 32; XAF needs at most 8 levels) |
| Huge text nodes | max_text_size (default 10 MiB per element) |
| Decompression bombs in gzip/zip input | decompressed size counted while streaming; ratio limit and optional absolute cap |
| Memory exhaustion by file size | transactions and lines are streamed; memory does not grow with the file |
| Millions of findings | max_findings_per_code and max_findings |
| Silent data corruption | no parser "recover" mode; malformed XML is an error with a position |
Hostile RGS workbook (.xlsx) |
the same DOCTYPE/ENTITY refusal for every XML part, a per-part size cap, cell references and row numbers beyond Excel's grid (16,384 columns, 1,048,576 rows) refused, row numbers must not decrease, and a gap of absent rows is expanded only as rows are consumed |
pyxaf only reads the paths and streams you pass in, and never writes anything except the export files you request.
DOCTYPE and ENTITY declarations are refused¶
No iteration of the Auditfile Financieel uses a DTD. pyxaf's XML engine, built on the standard
library's pyexpat, therefore raises ForbiddenConstructError as soon as it meets a DOCTYPE,
ENTITY, unparsed-entity or notation declaration, or an external entity reference:
>>> pyxaf.open(b'<?xml version="1.0"?><!DOCTYPE a [<!ENTITY x "y">]><auditfile/>')
Traceback (most recent call last):
...
pyxaf.errors.ForbiddenConstructError: DOCTYPE/ENTITY declarations are not allowed in an auditfile (refused for security)
pyxaf.validate() reports the same as an ERROR finding XAF2002 instead of raising. Refusing the
declarations rejects billion-laughs and XXE files in well under a millisecond, independent of the
Expat version. The check runs during detection too, so pyxaf.detect() refuses such files.
No recover mode¶
Some XML libraries offer a "recover" mode that skips over errors. On malformed auditfiles it
silently truncates text after a bare &, drops the rest of a truncated file, or accepts truncated
amounts, so totals come out wrong without any warning. pyxaf never uses it. Malformed XML raises
XmlSyntaxError with the line and column (finding XAF2001 in validate()), after delivering the
transactions before the damage. The opt-in repairs are narrow,
byte-level fixes for specific, common defects, and each one is reported as a finding.
Limits¶
Option of pyxaf.open() |
Default | Error |
|---|---|---|
max_depth |
32 levels | LimitExceededError |
max_text_size |
10 MiB (10,485,760 characters) per element | LimitExceededError |
max_decompressed_size |
none (bytes, for gzip/zip input) | LimitExceededError |
max_findings_per_code, max_findings |
100, 10,000 | further findings are counted, not kept |
Unless max_decompressed_size is given (which replaces it, so it can also raise the cap), gzip
and zip input may not inflate to more than 100 times its compressed size plus 1 MiB; the
decompressed bytes are counted while streaming, so a lying size
in a zip directory does not help an attacker. The guard is the same for paths, bytes and streams:
for a non-seekable stream, whose size is unknown, the ratio applies to the compressed bytes read so
far. A zip archive must contain exactly one file. The RGS
workbook reader caps every part at max_member_size (200 MB by default).
pyxaf.validate() reports an exceeded limit as finding XAF2003 (also when it is hit while
detecting the version) and returns the report checked so far. Integers longer than Python's
integer-string conversion limit (4,300 digits by default) are reported as invalid values, not
raised; pyxaf never changes that interpreter-wide limit.
lxml hardening¶
The core never uses lxml. The optional XSD validation (pyxaf[xsd]) does, and requires
lxml 6.1.3 or newer: before 6.1.0, lxml's iterparse() and ETCompatXMLParser resolved
external entities by default (CVE-2026-41066), and 6.1.3 also fixed the handling of parameter
entities. pyxaf additionally creates every lxml parser, for the bundled schemas and for the file,
with
and lxml only sees a file after pyxaf's own engine has accepted it (the XSD layer runs after the streaming pass). If you use lxml on auditfiles in your own code, use the same options.
Expat versions¶
Python's pyexpat may be linked against the system's Expat library. Expat versions before 2.7.2
have known denial-of-service weaknesses (see the
Python XML security notes). Keep
Python and Expat up to date; check with python -c "import pyexpat; print(pyexpat.EXPAT_VERSION)".
pyxaf's refusal of DTDs does not depend on the Expat version.
Privacy¶
Auditfiles contain personal data and confidential financial records: names and addresses of customers and suppliers, IBANs, VAT and Chamber of Commerce numbers, salaries in payroll journals, and the complete bookkeeping of a company. Sharing them can breach the GDPR (AVG), professional confidentiality and contracts.
- Never attach a real auditfile to a GitHub issue, discussion or pull request, not even "anonymised" by hand, and do not send one in a security report.
- Report the pyxaf and Python versions, the detected version (
pyxaf detect FILE), the exporting software, the finding codes and messages, and a minimal, hand-made snippet that reproduces the problem.CONTRIBUTING.mdexplains how to build one. - Finding messages and
valuefields quote data from the file (account IDs, amounts, names). Review a report before you share it.
pyxaf itself runs locally and sends nothing anywhere.