Skip to content

Security

CUF files are untrusted input. They arrive from subcontractors, clients and third-party software, and pycuf is often run on servers that process files from many sources. pycuf 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, 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 64; AFAS documents imports of up to 15 bundle levels)
Huge attribute values max_attribute_size (default 1 Mi characters)
Huge or tiny numbers (0e-1000000000000 is 17 characters, but a trillion digits in plain notation) numbers outside the range pycuf reads are CUF3018 and None; computations run in pycuf's own decimal context; Arrow export checks digits before building integers; CSV and JSONL refuse a computed number longer than 2,000 characters
Huge files max_size (default 256 MiB; real CUF files are rarely larger than a few MB)
Millions of findings max_findings_per_code and max_findings
Formulas in exported CSV (a description =HYPERLINK(…) opened in a spreadsheet) texts are exported exactly, by design; the interoperability guide explains how to open files from other parties safely
Silent data corruption no parser "recover" mode; malformed XML is an error with a position; repairs are opt-in and reported

pycuf only reads the paths and streams you pass in, never opens network connections, and never writes anything except the export files you request.

Python can be linked against a system Expat. Expat versions before 2.7.2 have known denial-of-service weaknesses (see the Python XML security notes); keep your Python and Expat up to date. pycuf --version (or pyexpat.EXPAT_VERSION) shows which Expat you have. pycuf's refusal of DTDs does not depend on the Expat version, and the default max_depth of 64 stops the deep nesting that drives the memory weakness fixed in Expat 2.7.2 (CVE-2025-59375): keep it small when your Expat is older.

DOCTYPE and ENTITY declarations are refused

CUF-XML never uses a DTD. pycuf's parser, built on the standard library's pyexpat, raises ForbiddenConstructError as soon as it meets a DOCTYPE, ENTITY, unparsed-entity or notation declaration, or an external entity reference:

>>> pycuf.read(b'<?xml version="1.0"?><!DOCTYPE a [<!ENTITY x "y">]><CUF/>')
Traceback (most recent call last):
  ...
pycuf.errors.ForbiddenConstructError: DOCTYPE/ENTITY declarations are not allowed in CUF-XML (refused for security)

pycuf.validate() reports the same as finding CUF2002 instead of raising.

Optional extras

The Arrow, polars, pandas and Parquet extras only receive data pycuf has already parsed and validated; they never see the XML. Keep them up to date like any other dependency.

Personal and confidential data

Estimates contain client names and addresses and commercially sensitive prices. Never attach a real CUF file to an issue or pull request; see CONTRIBUTING.md.