pyblock/migrate_config.py
GaltRanch 12ccdd5c19 Harden pickle deserialization with SafeUnpickler in migrate_config.py
Replace raw pickle.load() with a restricted SafeUnpickler that only
allows basic Python types (dict, list, str, int, etc.), blocking
arbitrary code execution from tampered pickle files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:58:49 -03:00

147 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""
Migration script: Convert PyBLOCK config files from pickle to JSON format.
This script finds all .conf files used by PyBLOCK, reads them as pickle,
and rewrites them as JSON. A backup of each original file is created
with a .pickle.bak extension.
Usage:
python3 migrate_config.py [directory]
If no directory is specified, it searches the current directory and
common PyBLOCK config locations.
"""
import io
import json
import os
import pickle
import shutil
import sys
class SafeUnpickler(pickle.Unpickler):
"""Restricted unpickler that only allows basic Python types."""
SAFE_CLASSES = {
('builtins', 'dict'),
('builtins', 'list'),
('builtins', 'set'),
('builtins', 'tuple'),
('builtins', 'str'),
('builtins', 'int'),
('builtins', 'float'),
('builtins', 'bool'),
('builtins', 'bytes'),
('builtins', 'type'),
}
def find_class(self, module, name):
if (module, name) not in self.SAFE_CLASSES:
raise pickle.UnpicklingError(
f"Blocked unsafe class: {module}.{name}"
)
return super().find_class(module, name)
def safe_pickle_load(f):
"""Load pickle data using restricted unpickler."""
return SafeUnpickler(f).load()
def find_conf_files(search_dirs):
"""Find all .conf files in the given directories."""
conf_files = []
for search_dir in search_dirs:
if not os.path.isdir(search_dir):
continue
for root, _, files in os.walk(search_dir):
for f in files:
if f.endswith('.conf'):
conf_files.append(os.path.join(root, f))
return conf_files
def is_pickle_file(filepath):
"""Check if a file is in pickle format (not valid JSON)."""
try:
with open(filepath, 'r') as f:
json.load(f)
return False # Already JSON
except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
try:
with open(filepath, 'rb') as f:
safe_pickle_load(f)
return True # Valid pickle
except Exception:
return False # Neither pickle nor JSON
def migrate_file(filepath):
"""Migrate a single .conf file from pickle to JSON."""
if not is_pickle_file(filepath):
return False, "already JSON or not a valid pickle file"
try:
# Read pickle data using safe unpickler
with open(filepath, 'rb') as f:
data = safe_pickle_load(f)
# Create backup
backup_path = filepath + '.pickle.bak'
shutil.copy2(filepath, backup_path)
# Write as JSON
with open(filepath, 'w') as f:
json.dump(data, f, indent=2, default=str)
return True, f"migrated (backup: {backup_path})"
except Exception as e:
return False, f"error: {e}"
def main():
if len(sys.argv) > 1:
search_dirs = [sys.argv[1]]
else:
# Search common PyBLOCK config locations
search_dirs = [
'.',
'config',
'pybitblock',
'pybitblock/config',
'pybitblock/SPV',
'pybitblock/SPV/config',
]
conf_files = find_conf_files(search_dirs)
if not conf_files:
print("No .conf files found.")
return
print(f"Found {len(conf_files)} config file(s):\n")
migrated = 0
skipped = 0
errors = 0
for filepath in sorted(conf_files):
success, message = migrate_file(filepath)
status = "OK" if success else "SKIP"
if "error" in message:
status = "ERR"
errors += 1
elif success:
migrated += 1
else:
skipped += 1
print(f" [{status}] {filepath} - {message}")
print(f"\nResults: {migrated} migrated, {skipped} skipped, {errors} errors")
if __name__ == '__main__':
main()