| # sklearn-pmml-model: Incorrect SparseArray Indices/Entries pairing in `parse_sparse_array()` -> IndexError DoS or silent model-output corruption |
|
|
| **Target repo:** https://github.com/iamDecode/sklearn-pmml-model |
| **Version confirmed:** v1.0.8 (PyPI), verified byte-identical to current GitHub `master` `sklearn_pmml_model/base.py` via `diff` against a freshly fetched copy of `raw.githubusercontent.com`. |
| **CWE:** CWE-129 (Improper Validation of Array Index) / incorrect array index usage. |
| **Reported:** 2026-07-12 |
|
|
| ## Summary |
|
|
| `parse_sparse_array()` in `sklearn_pmml_model/base.py` mis-pairs the `<Indices>` |
| and `<Entries>` elements of a PMML `<SparseArray>`. Per the PMML/DMG spec, |
| position *i* of `<Indices>` names the destination slot for position *i* of |
| `<Entries>` (i.e. the correct code is `values[indices[i]] = entries[i]`, e.g. |
| via `zip`/`enumerate`). The actual code instead reuses the **value** of each |
| index as if it were also a valid position into `entries`: |
|
|
| ```python |
| for index in indices: |
| values[index] = entries[index] |
| ``` |
|
|
| This is wrong for essentially any non-trivial sparse array (any case where |
| the index values are not exactly the sequence `0,1,2,...,len(entries)-1`), |
| which is the entire point of using a `SparseArray` instead of a dense |
| `Array` in the first place. |
|
|
| This bug is reached through the library's own public, documented API |
| (`sklearn_pmml_model.auto_detect.auto_detect_estimator()`, and the |
| `PMMLSVR`/`PMMLSVC` classes that use it), the same call path as an |
| already-filed, separate SparseArray `@n` uncontrolled-allocation finding. |
| **This is a distinct defect**: different root cause (indexing/pairing logic, |
| not pre-allocation size), different line (`base.py` ~line 395 vs. the `@n` |
| allocation site), and **not** fixed by that finding's suggested remediation |
| (bounding/capping `@n`) β this bug fires or silently corrupts data |
| regardless of how small `@n` is. |
|
|
| ## Root cause |
|
|
| File: `sklearn_pmml_model/base.py`, function `parse_sparse_array()` |
| (approx. lines 368-396 of current master): |
|
|
| ```python |
| values = [0] * int(array.get('n')) |
| indices = [int(i) - 1 for i in array.find('Indices').text.split(' ')] |
| ... |
| entries = [float(x) for x in element.text.split(' ')] # (or int, per type) |
| ... |
| for index in indices: |
| values[index] = entries[index] |
| ``` |
|
|
| `indices` holds the 0-based (converted from PMML's 1-based) *target |
| positions* named by `<Indices>`. `entries` is a separate, independently |
| sized list parsed from `<Entries>` / `<NUM-Entries>` / `<REAL-Entries>` / |
| `<INT-Entries>`. The code indexes `entries` using the *index value itself* |
| rather than the *position within the indices list*, so: |
|
|
| - If any index value is `>= len(entries)`, `entries[index]` raises an |
| uncaught `IndexError`. |
| - If index values are within range of `len(entries)` but not exactly |
| `0, 1, 2, ..., len(entries)-1` in order, the wrong entries silently land |
| in the wrong slots β no exception, but corrupted values. |
|
|
| Two security-relevant, independently reachable outcomes: |
|
|
| 1. **IndexError crash / DoS** β whenever any index value is |
| `>= len(entries)` (the normal case for genuinely sparse data: a large |
| feature space with few non-zero entries), parsing raises an uncaught |
| `IndexError` and aborts the caller. No huge-allocation trick needed β |
| `n` can be a completely ordinary, small value. |
| 2. **Silent model-output corruption** β whenever index values happen to |
| stay in range but are not in strictly sequential order, the resulting |
| support-vector (or coefficient) values are silently wrong. An |
| attacker-crafted PMML file loads "successfully" but yields corrupted |
| predictions at inference time, undermining the integrity of the loaded |
| model with no visible error. |
|
|
| ## Proof of Concept |
|
|
| Three minimal (~1KB) `SupportVectorMachineModel` PMML files, each with a |
| single support vector encoded as a `<SparseArray>`, loaded via the |
| library's public, documented API, |
| `sklearn_pmml_model.auto_detect.auto_detect_estimator()`. |
|
|
| ### 1. `wrong_indices.pmml` β IndexError DoS |
| |
| ```xml |
| <SparseArray type="real" n="5"> |
| <Indices>1 3 5</Indices> |
| <Entries>10 20 30</Entries> |
| </SparseArray> |
| ``` |
| |
| Real, captured output loading this file: |
| |
| ``` |
| Traceback (most recent call last): |
| File "<string>", line 6, in <module> |
| clf = auto_detect_estimator('/home/kali/hunt-workspace/pmml-index-bug/wrong_indices.pmml') |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/auto_detect/base.py", line 40, in auto_detect_estimator |
| return auto_detect_regressor(pmml, **kwargs) |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/auto_detect/base.py", line 129, in auto_detect_regressor |
| return parse(f) |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/auto_detect/base.py", line 123, in parse |
| return reg(pmml, **kwargs) |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/svm/_classes.py", line 295, in __init__ |
| PMMLBaseSVM.__init__(self) |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/svm/_base.py", line 56, in __init__ |
| get_vectors(vector_dictionary, s) for s in self.support_ |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/svm/_base.py", line 109, in get_vectors |
| return np.array(parse_array(array)) |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/base.py", line 330, in parse_array |
| return parse_sparse_array(array) |
| File "/home/kali/pmml-verify-build/sklearn-pmml-model/sklearn_pmml_model/base.py", line 395, in parse_sparse_array |
| values[index] = entries[index] |
| IndexError: list index out of range |
| Exception: <class 'IndexError'> list index out of range |
| ``` |
| |
| ### 2. `silent_corruption.pmml` β silent model-output corruption |
| |
| ```xml |
| <SparseArray type="real" n="3"> |
| <Indices>2 3 1</Indices> |
| <Entries>10 20 30</Entries> |
| </SparseArray> |
| ``` |
| |
| Real, captured output loading this file: |
| |
| ``` |
| Loaded OK. support_vectors_ = [[10. 20. 30.]] |
| Expected per PMML spec (Indices 1-based positional pairing with Entries): [[30, 10, 20]] |
| ``` |
| |
| No exception is raised, but the loaded support-vector values are wrong, |
| silently corrupting any predictions made with this model. |
| |
| ### 3. `sequential_control.pmml` β negative control |
| |
| ```xml |
| <SparseArray type="real" n="3"> |
| <Indices>1 2 3</Indices> |
| <Entries>10 20 30</Entries> |
| </SparseArray> |
| ``` |
| |
| Real, captured output loading this file: |
| |
| ``` |
| Negative control (sequential Indices=1,2,3): support_vectors_ = [[10. 20. 30.]] -- correct, no bug visible here |
| ``` |
| |
| This case loads correctly, because when the index *value* happens to equal |
| its *position*, the buggy `entries[index]` lookup accidentally coincides |
| with the correct `entries[position]` lookup. This is why ordinary/unit-test |
| PMML fixtures β which tend to use sequential toy data β would not have |
| surfaced this bug. |
| |
| ## Verification environment |
| |
| - Real execution against a fresh checkout of |
| `iamDecode/sklearn-pmml-model` (v1.0.8) at |
| `/home/kali/pmml-verify-build/sklearn-pmml-model`. |
| - Confirmed byte-identical to current GitHub `master`'s |
| `sklearn_pmml_model/base.py` via `diff` against a freshly fetched copy |
| from `raw.githubusercontent.com` (included here as `latest_base.py` for |
| identity confirmation). |
| - All three PoC files loaded through the library's own public, |
| documented API entry point, `auto_detect_estimator()` β no internal/private |
| functions called directly. |
| |
| ## Dedup check |
| |
| GitHub code/issue search API against `iamDecode/sklearn-pmml-model` for |
| `"SparseArray"` and `"IndexError"` both returned `total_count: 0`. |
| `repos/.../security-advisories` returned `[]`. Checked 2026-07-12. |
| |
| This finding is a **distinct** defect from an already-filed SparseArray `@n` |
| uncontrolled-allocation finding against the same project: different root |
| cause (Indices/Entries pairing logic vs. unbounded pre-allocation of the |
| `values` list), different line, and not remediated by that finding's fix |
| (bounding `@n`) since this bug is independent of the size of `@n`. |
| |
| ## Suggested fix |
| |
| Pair `indices` and `entries` positionally, per the PMML spec, e.g.: |
| |
| ```python |
| for pos, index in enumerate(indices): |
| values[index] = entries[pos] |
| ``` |
| |
| (equivalently, `for index, entry in zip(indices, entries): values[index] = entry`), |
| and validate `len(indices) == len(entries)` and that every index is within |
| bounds of `values`, raising a clear `ValueError`/`Exception` rather than an |
| uncaught `IndexError` for malformed input. |
|
|
| ## Files in this repo |
|
|
| - `wrong_indices.pmml` β PoC 1 (IndexError DoS) |
| - `silent_corruption.pmml` β PoC 2 (silent corruption) |
| - `sequential_control.pmml` β PoC 3 (negative control) |
| - `latest_base.py` β freshly fetched copy of current GitHub master |
| `sklearn_pmml_model/base.py`, used to confirm the verified build matches |
| the currently released code. |
|
|