mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
203 lines
7.2 KiB
Python
203 lines
7.2 KiB
Python
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
|
|
from services.packs.pack_archive import PackArchive
|
|
from services.packs.pack_manifest import (
|
|
MAX_MANIFEST_FILES,
|
|
PackError,
|
|
validate_manifest_integrity,
|
|
validate_pack_metadata,
|
|
)
|
|
from services.packs.pack_registry import PackRegistry, _validate_pack_segment
|
|
from services.packs.pack_types import PackMetadata, PackType
|
|
|
|
|
|
class TestPackSecurity(unittest.TestCase):
|
|
def setUp(self):
|
|
self.test_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.test_dir)
|
|
|
|
def test_metadata_validation(self):
|
|
valid = {
|
|
"name": "test-pack",
|
|
"version": "1.0.0",
|
|
"type": "preset",
|
|
"author": "tester",
|
|
"min_moltbot_version": "0.1.0",
|
|
}
|
|
self.assertEqual(validate_pack_metadata(valid), valid)
|
|
|
|
invalid = valid.copy()
|
|
del invalid["name"]
|
|
with self.assertRaisesRegex(PackError, "Missing required field"):
|
|
validate_pack_metadata(invalid)
|
|
|
|
invalid_type = valid.copy()
|
|
invalid_type["type"] = "malicious"
|
|
with self.assertRaisesRegex(PackError, "Invalid pack type"):
|
|
validate_pack_metadata(invalid_type)
|
|
|
|
def test_integrity_check(self):
|
|
# Create a dummy file
|
|
fpath = os.path.join(self.test_dir, "test.txt")
|
|
with open(fpath, "w") as f:
|
|
f.write("hello")
|
|
|
|
# Correct hash for "hello" is 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
|
|
valid_manifest = {
|
|
"files": [
|
|
{
|
|
"path": "test.txt",
|
|
"sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
|
}
|
|
]
|
|
}
|
|
self.assertEqual(validate_manifest_integrity(self.test_dir, valid_manifest), [])
|
|
|
|
# Tampered file
|
|
with open(fpath, "w") as f:
|
|
f.write("hacked")
|
|
|
|
errors = validate_manifest_integrity(self.test_dir, valid_manifest)
|
|
self.assertTrue(any("Hash mismatch" in e for e in errors))
|
|
|
|
def test_path_traversal_prevention(self):
|
|
zip_path = os.path.join(self.test_dir, "traversal.zip")
|
|
with zipfile.ZipFile(zip_path, "w") as zf:
|
|
zf.writestr("../../../etc/passwd", "root:x:0:0...")
|
|
|
|
with self.assertRaisesRegex(PackError, "Unsafe filename"):
|
|
PackArchive.extract_pack(zip_path, os.path.join(self.test_dir, "out"))
|
|
|
|
def test_symlink_rejection(self):
|
|
# Python zipfile doesn't make it easy to create symlinks by default without external tools or direct info manipulation.
|
|
# We simulate by mocking ZipInfo or constructing a crafted zip manually if needed.
|
|
# Here we'll try to manually set the external_attr.
|
|
|
|
zip_path = os.path.join(self.test_dir, "symlink.zip")
|
|
zinfo = zipfile.ZipInfo("link")
|
|
zinfo.create_system = 3 # Unix
|
|
zinfo.external_attr = 0xA000 << 16 | 0o777 # S_IFLNK
|
|
|
|
with zipfile.ZipFile(zip_path, "w") as zf:
|
|
zf.writestr(zinfo, "target")
|
|
|
|
with self.assertRaisesRegex(PackError, "Symlinks not allowed"):
|
|
PackArchive.extract_pack(zip_path, os.path.join(self.test_dir, "out"))
|
|
|
|
def test_max_files_limit(self):
|
|
# We can mock ZipFile context to avoid creating huge file
|
|
# But honestly, creating 1001 empty entries in memory is fast.
|
|
zip_path = os.path.join(self.test_dir, "huge.zip")
|
|
with zipfile.ZipFile(zip_path, "w") as zf:
|
|
for i in range(MAX_MANIFEST_FILES + 5):
|
|
zf.writestr(f"f{i}.txt", "")
|
|
|
|
with self.assertRaisesRegex(PackError, "Too many files"):
|
|
PackArchive.extract_pack(zip_path, os.path.join(self.test_dir, "out"))
|
|
|
|
|
|
class TestPackRegistryPathTraversal(unittest.TestCase):
|
|
"""Test that pack_registry rejects path traversal in name/version."""
|
|
|
|
def setUp(self):
|
|
self.test_dir = tempfile.mkdtemp()
|
|
self.registry = PackRegistry(self.test_dir)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.test_dir)
|
|
|
|
def test_validate_segment_rejects_dotdot(self):
|
|
with self.assertRaises(PackError):
|
|
_validate_pack_segment("..", "name")
|
|
|
|
def test_validate_segment_rejects_slash(self):
|
|
with self.assertRaises(PackError):
|
|
_validate_pack_segment("../../etc", "name")
|
|
|
|
def test_validate_segment_rejects_backslash(self):
|
|
with self.assertRaises(PackError):
|
|
_validate_pack_segment("..\\..\\etc", "name")
|
|
|
|
def test_validate_segment_rejects_empty(self):
|
|
with self.assertRaises(PackError):
|
|
_validate_pack_segment("", "version")
|
|
|
|
def test_validate_segment_accepts_valid(self):
|
|
_validate_pack_segment("my-pack_v2.1", "name") # Should not raise
|
|
|
|
def test_uninstall_rejects_traversal(self):
|
|
with self.assertRaises(PackError):
|
|
self.registry.uninstall_pack("../../etc", "passwd")
|
|
|
|
def test_get_pack_path_rejects_traversal(self):
|
|
with self.assertRaises(PackError):
|
|
self.registry.get_pack_path("../../../tmp", "evil")
|
|
|
|
def test_uninstall_rejects_dot_dot_version(self):
|
|
with self.assertRaises(PackError):
|
|
self.registry.uninstall_pack("legit-name", "..")
|
|
|
|
|
|
class TestPackRegistryInstallTraversal(unittest.TestCase):
|
|
"""Test that install_pack rejects traversal sequences in zip metadata."""
|
|
|
|
def setUp(self):
|
|
self.test_dir = tempfile.mkdtemp()
|
|
self.registry = PackRegistry(self.test_dir)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.test_dir)
|
|
|
|
def _make_pack_zip(self, name, version):
|
|
"""Create a minimal valid pack zip with the given name/version in metadata."""
|
|
import hashlib
|
|
|
|
zip_path = os.path.join(self.test_dir, "test.zip")
|
|
pack_meta = {
|
|
"name": name,
|
|
"version": version,
|
|
"type": "preset",
|
|
"author": "tester",
|
|
"min_moltbot_version": "0.1.0",
|
|
}
|
|
pack_json = json.dumps(pack_meta).encode("utf-8")
|
|
pack_hash = hashlib.sha256(pack_json).hexdigest()
|
|
manifest = {"files": [{"path": "pack.json", "sha256": pack_hash}]}
|
|
|
|
with zipfile.ZipFile(zip_path, "w") as zf:
|
|
zf.writestr("pack.json", pack_json)
|
|
zf.writestr("manifest.json", json.dumps(manifest))
|
|
return zip_path
|
|
|
|
def test_install_rejects_traversal_in_name(self):
|
|
zip_path = self._make_pack_zip("../../etc", "1.0.0")
|
|
with self.assertRaises(PackError):
|
|
self.registry.install_pack(zip_path)
|
|
|
|
def test_install_rejects_traversal_in_version(self):
|
|
zip_path = self._make_pack_zip("legit-pack", "../../../tmp")
|
|
with self.assertRaises(PackError):
|
|
self.registry.install_pack(zip_path)
|
|
|
|
def test_install_rejects_dotdot_name(self):
|
|
zip_path = self._make_pack_zip("..", "1.0.0")
|
|
with self.assertRaises(PackError):
|
|
self.registry.install_pack(zip_path)
|
|
|
|
def test_install_accepts_valid_metadata(self):
|
|
zip_path = self._make_pack_zip("my-pack", "1.0.0")
|
|
meta = self.registry.install_pack(zip_path)
|
|
self.assertEqual(meta["name"], "my-pack")
|
|
self.assertEqual(meta["version"], "1.0.0")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|