| # sklearn-pmml-model SparseArray `n` Attribute Unbounded Allocation |
|
|
| **Status:** Preparing for Huntr submission |
| **Package:** [`sklearn-pmml-model`](https://pypi.org/project/sklearn-pmml-model/) (PyPI) β pure-Python PMML model loader |
| **File / function:** `sklearn_pmml_model/base.py`, `parse_sparse_array()` |
| **Class:** CWE-789 (Convergent Untrusted-Length Allocation) |
| **Severity:** High β a ~1.3KB model file loaded through the library's real, public, documented API consumes ~450MB of memory and takes several seconds; a larger declared value causes the process to be OOM-killed or hang indefinitely. |
|
|
| ## Summary |
|
|
| PMML (Predictive Model Markup Language) is an XML-based standard for exchanging trained ML models. `sklearn-pmml-model` parses these files using Python's stdlib `xml.etree.cElementTree`. When it encounters a `<SparseArray>` element (used for sparse-encoded data, e.g. support vector storage in SVM models), it allocates a buffer sized directly from the element's declared `n` attribute before validating it: |
|
|
| ```python |
| # sklearn_pmml_model/base.py |
| def parse_sparse_array(array): |
| ... |
| values = [0] * int(array.get('n')) |
| indices = [int(i) - 1 for i in array.find('Indices').text.split(' ')] |
| ... |
| ``` |
|
|
| `array.get('n')` is fully attacker-controlled in a crafted PMML file. There is no check on this value before it drives a Python list allocation β and Python lists of integers are notably memory-hungry per element (each entry is a full boxed `int` object plus a pointer), making this more severe than an equivalent-sized numpy buffer. |
|
|
| ## Two classic XML attacks were checked first and found NOT applicable |
|
|
| Before finding this schema-specific issue, the two well-known XML parsing attack classes were tested against the library's use of `xml.etree.cElementTree`, for due diligence: |
|
|
| - **Billion Laughs (entity expansion)** β blocked. Modern Python's bundled `expat` enforces a built-in amplification-factor limit; a crafted DOCTYPE with nested entities raises `xml.etree.ElementTree.ParseError: limit on input amplification factor (from DTD and entities) breached` in well under a second. |
| - **XXE (external entity / local file read)** β blocked. `ElementTree` refuses external entity references by default (`reference to external entity in attribute`), which is documented, long-standing safe-by-default stdlib behavior. |
|
|
| This finding is **not** either of those β it's a PMML-schema-specific gap in how `<SparseArray>`'s `n` attribute is trusted. |
|
|
| ## Proof of Concept |
|
|
| `poc_sklearn_pmml_sparsearray_bomb.py` builds a complete, valid, minimal PMML support-vector regression model β the kind loadable via the library's own public API, `sklearn_pmml_model.svm.PMMLNuSVR` β containing one `<REAL-SparseArray>` support vector that declares `n="20000000"` but has only one real `<Indices>`/`<REAL-Entries>` value. |
|
|
| ```bash |
| pip install sklearn-pmml-model numpy scikit-learn |
| python poc_sklearn_pmml_sparsearray_bomb.py |
| ``` |
|
|
| ### Result |
|
|
| ``` |
| Wrote poc_sparsearray_bomb.pmml: 1266 bytes, declares SparseArray n=20,000,000 (only 1 real index/value) |
| |
| Loading via the real public API: PMMLNuSVR(pmml='poc_sparsearray_bomb.pmml') ... |
| -> loaded, time=8.82s, peak RSS=450.8 MB |
| ``` |
|
|
| The script defaults to `n=20,000,000` so it completes in a reasonable time while still clearly demonstrating the amplification (~1:370,000, file bytes to RSS bytes). During research, `n=500,000,000` in the same 1266-byte file caused the load to either hang past a 30-second timeout without finishing, or be killed outright by the OS's OOM killer, depending on available memory β see the `HUGE_N` constant in the script to reproduce that more severe outcome. |
|
|
| Note this loads through the model's **real, documented, public loading path** β `PMMLNuSVR(pmml=path)` β not an internal-function-only proof; any application that loads a PMML file with this library and does not pre-validate it is affected exactly as tested here. |
|
|
| ## Impact |
|
|
| Any service that loads PMML files using `sklearn-pmml-model` β for example, a model-serving platform or MLOps pipeline that accepts PMML models from users or third parties β can be forced into multi-hundred-megabyte-to-multi-gigabyte memory consumption, or an outright process kill, by a file of a little over a kilobyte. |
|
|
| ## Suggested fix |
|
|
| In `parse_sparse_array()`, validate `n` against the actual number of entries found in the `<Indices>`/`<Entries>` children (or against a configurable sane maximum) before allocating `[0] * n`, and raise a clear error on mismatch. |
|
|
| ## Disclosure |
|
|
| Please do not use this PoC against production systems you do not own or have explicit permission to test. |
|
|