Skip to content

Commit 2fe6ed3

Browse files
sytelusclaude
andcommitted
fix unsafe pickle deserialization vulnerability in ZMQ transport
Add HMAC-SHA256 signing to all ZMQ messages so that only processes sharing the same key can exchange pickle payloads. Unsigned or forged messages are rejected before reaching pickle.loads(). Also bind the Publication socket to 127.0.0.1 by default instead of all interfaces, and expand the README security notice to cover the network attack surface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6fe1359 commit 2fe6ed3

2 files changed

Lines changed: 69 additions & 16 deletions

File tree

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,28 @@ TensorWatch is under heavy development with a goal of providing a platform for d
1414
pip install tensorwatch
1515
```
1616

17-
TensorWatch supports Python 3.x and is tested with PyTorch 0.4-1.x. Most features should also work with TensorFlow eager tensors. TensorWatch uses graphviz to create network diagrams and depending on your platform sometime you might need to manually [install](https://graphviz.gitlab.io/download/) it.
18-
19-
### Security Notice
20-
21-
> **Caution:** TensorWatch persists stream data with Python's `pickle` serialization. Unpickling content from untrusted sources can execute arbitrary code. Only open TensorWatch pickle files (for example `.log` or `.pkl`) that you created yourself or that come from a source you fully trust.
17+
TensorWatch supports Python 3.x and is tested with PyTorch 0.4-1.x. Most features should also work with TensorFlow eager tensors. TensorWatch uses graphviz to create network diagrams and depending on your platform sometime you might need to manually [install](https://graphviz.gitlab.io/download/) it.
18+
19+
### Security Notice
20+
21+
> **Caution:** TensorWatch uses Python's `pickle` serialization for both file
22+
> persistence and network communication over ZeroMQ sockets. Pickle
23+
> deserialization can execute arbitrary code.
24+
>
25+
> - **Files:** Only open TensorWatch pickle files (`.log`, `.pkl`) that you
26+
> created yourself or that come from a source you fully trust.
27+
> - **Network:** When `tw.Watcher()` is instantiated, it opens local TCP
28+
> sockets (default ports 40859 and 41459) for real-time streaming and the
29+
> Lazy Logging query interface. Messages are HMAC-signed to reject payloads
30+
> from unauthorized processes, but the Lazy Logging feature intentionally
31+
> evaluates Python expressions sent by connected clients. **Do not expose
32+
> TensorWatch ports to untrusted networks or users.** TensorWatch is a
33+
> development and debugging tool and should not be used in production or
34+
> multi-tenant environments.
35+
> - **Lazy Logging:** The `expr` parameter in `create_stream()` is evaluated
36+
> with Python's `eval()`. This is by design to enable interactive debugging,
37+
> but it means any client that can authenticate and connect can execute
38+
> arbitrary Python code in the Watcher process.
2239
2340
## How to Use It
2441

tensorwatch/zmq_wrapper.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import zmq
55
import errno
66
import pickle
7+
import hmac
8+
import hashlib
9+
import os
710
from zmq.eventloop import ioloop, zmqstream
811
import zmq.utils.monitor
912
import functools, sys, logging
@@ -17,6 +20,39 @@ class ZmqWrapper:
1720
_ioloop:ioloop.IOLoop = None
1821
_start_event:Event = None
1922
_ioloop_block:Event = None # indicates if there is any blocking IOLoop call in progress
23+
_hmac_key:bytes = None
24+
25+
@staticmethod
26+
def get_hmac_key() -> bytes:
27+
"""Get or generate the HMAC signing key used to authenticate ZMQ messages.
28+
Set ZmqWrapper._hmac_key before calling initialize() to use a specific
29+
key (e.g. for multi-process setups where Watcher and WatcherClient run
30+
in separate processes).
31+
"""
32+
if ZmqWrapper._hmac_key is None:
33+
ZmqWrapper._hmac_key = os.urandom(32)
34+
return ZmqWrapper._hmac_key
35+
36+
@staticmethod
37+
def sign_and_dumps(obj) -> bytes:
38+
"""Serialize obj with pickle and prepend an HMAC-SHA256 signature."""
39+
payload = pickle.dumps(obj)
40+
sig = hmac.new(ZmqWrapper.get_hmac_key(), payload, hashlib.sha256).digest()
41+
return sig + payload
42+
43+
@staticmethod
44+
def verify_and_loads(signed_data: bytes):
45+
"""Verify HMAC-SHA256 signature and deserialize with pickle.
46+
Raises ValueError if the signature does not match.
47+
"""
48+
if len(signed_data) < 32:
49+
raise ValueError("Message too short to contain HMAC signature")
50+
sig = signed_data[:32]
51+
payload = signed_data[32:]
52+
expected = hmac.new(ZmqWrapper.get_hmac_key(), payload, hashlib.sha256).digest()
53+
if not hmac.compare_digest(sig, expected):
54+
raise ValueError("HMAC verification failed - rejecting untrusted message")
55+
return pickle.loads(payload)
2056

2157
@staticmethod
2258
def initialize():
@@ -107,7 +143,7 @@ def wrapper(f, r, *kargs, **kwargs):
107143
ZmqWrapper._ioloop.add_callback(f_wrapped)
108144

109145
class Publication:
110-
def __init__(self, port, host='*', block_until_connected=True):
146+
def __init__(self, port, host='127.0.0.1', block_until_connected=True):
111147
# define vars
112148
self._socket = None
113149
self._mon_socket = None
@@ -138,8 +174,8 @@ def _send_multipart(self, parts):
138174
return self._socket.send_multipart(parts)
139175

140176
def send_obj(self, obj, topic=''):
141-
ZmqWrapper._io_loop_call(False, self._send_multipart,
142-
[topic.encode(), pickle.dumps(obj)])
177+
ZmqWrapper._io_loop_call(False, self._send_multipart,
178+
[topic.encode(), ZmqWrapper.sign_and_dumps(obj)])
143179

144180
def _on_mon(self, msg):
145181
ev = zmq.utils.monitor.parse_monitor_message(msg)
@@ -172,7 +208,7 @@ def callback_wrapper(weak_callback, msg):
172208
[topic, obj_s] = msg # pylint: disable=unused-variable
173209
try:
174210
if weak_callback and weak_callback():
175-
weak_callback()(pickle.loads(obj_s))
211+
weak_callback()(ZmqWrapper.verify_and_loads(obj_s))
176212
except Exception as ex:
177213
logging.exception('Error in subscription callback')
178214
raise
@@ -202,8 +238,8 @@ def callback_wrapper(weak_callback, msg):
202238
def _receive_obj(self):
203239
[topic, obj_s] = self._socket.recv_multipart() # pylint: disable=unbalanced-tuple-unpacking
204240
if topic != self.topic:
205-
raise ValueError('Expected topic: %s, Received topic: %s' % (topic, self.topic))
206-
return pickle.loads(obj_s)
241+
raise ValueError('Expected topic: %s, Received topic: %s' % (topic, self.topic))
242+
return ZmqWrapper.verify_and_loads(obj_s)
207243

208244
def receive_obj(self):
209245
return ZmqWrapper._io_loop_call(True, self._receive_obj)
@@ -239,13 +275,13 @@ def callback_wrapper(callback, msg):
239275

240276
[obj_s] = msg
241277
try:
242-
ret = callback(self, pickle.loads(obj_s))
278+
ret = callback(self, ZmqWrapper.verify_and_loads(obj_s))
243279
# we must send reply to complete the cycle
244-
self._socket.send_multipart([pickle.dumps((ret, None))])
280+
self._socket.send_multipart([ZmqWrapper.sign_and_dumps((ret, None))])
245281
except Exception as ex:
246282
logging.exception('ClientServer call raised exception')
247283
# we must send reply to complete the cycle
248-
self._socket.send_multipart([pickle.dumps((None, ex))])
284+
self._socket.send_multipart([ZmqWrapper.sign_and_dumps((None, ex))])
249285

250286
utils.debug_log('Server sent response', verbosity=6)
251287

@@ -274,12 +310,12 @@ def callback_wrapper(callback, msg):
274310

275311
def send_obj(self, obj):
276312
ZmqWrapper._io_loop_call(False, self._socket.send_multipart,
277-
[pickle.dumps(obj)])
313+
[ZmqWrapper.sign_and_dumps(obj)])
278314

279315
def receive_obj(self):
280316
# pylint: disable=unpacking-non-sequence
281317
[obj_s] = ZmqWrapper._io_loop_call(True, self._socket.recv_multipart)
282-
return pickle.loads(obj_s)
318+
return ZmqWrapper.verify_and_loads(obj_s)
283319

284320
def request(self, req_obj):
285321
utils.debug_log('Client sending request...', verbosity=6)

0 commit comments

Comments
 (0)