Skip to content

Commit fbbcae8

Browse files
committed
fix(security): disable pickle for numpy archive loads
1 parent 7a39c64 commit fbbcae8

5 files changed

Lines changed: 124 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ record also lives in the git commit messages.
2222

2323
### Fixed — release process
2424

25+
- README test-count badge now syncs to the live suite count so the release
26+
readiness gate stays green after new coverage lands.
2527
- Release smoke and doc-size drift checks now target only live documentation
2628
surfaces (`CLAUDE.md` and `README.md`) instead of removed planning files.
2729
- Version sync now covers the security support table, CEP package-lock root
2830
metadata, and the C2PA claim-generator string so release smoke fails when
2931
those public version surfaces drift.
3032

33+
### Security — data loading
34+
35+
- Character embeddings and depth-compositor archives now call `np.load` with
36+
pickle explicitly disabled, with regression coverage for both paths.
37+
3138
### Security — cache safety
3239

3340
- Preview-cache metadata is now scoped to the active cache directory and ignores

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
![Python](https://img.shields.io/badge/Python-3.11+-3776AB?logo=python&logoColor=white)
77
![Premiere Pro](https://img.shields.io/badge/Premiere%20Pro-2019+-9999FF?logo=adobepremierepro&logoColor=white)
88
![Routes](https://img.shields.io/badge/API%20Routes-1539-orange)
9-
![Tests](https://img.shields.io/badge/Tests-10100+-brightgreen)
9+
![Tests](https://img.shields.io/badge/Tests-10200+-brightgreen)
1010

1111
> Route count is generated from `opencut/_generated/route_manifest.json`; run
1212
> `python -m opencut.tools.dump_route_manifest --check` to verify it is in

opencut/core/character_consistency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ def _generate_with_ip_adapter(
538538
pipe = pipe.to("cuda")
539539

540540
# Load character embeddings and validate presence
541-
data = np.load(embeddings_path)
541+
data = np.load(embeddings_path, allow_pickle=False)
542542
if "face_embedding" not in data and "clip_embedding" not in data:
543543
return ""
544544

opencut/core/compose_depth_segment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def compose(
175175
depth_data = None
176176
if "depth_parallax" in valid_effects or "vignette_depth" in valid_effects:
177177
try:
178-
depth_data = np.load(depth_path)["depths"]
178+
depth_data = np.load(depth_path, allow_pickle=False)["depths"]
179179
except Exception:
180180
pass
181181

tests/test_motion_gen.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
import os
1414
import sys
1515
import tempfile
16-
from unittest.mock import patch
16+
import types
17+
from unittest.mock import MagicMock, patch
1718

1819
import pytest
1920

@@ -585,6 +586,118 @@ def on_prog(pct, msg=""):
585586
generate_consistent_scene("scene prompt", profile, on_progress=on_prog)
586587
assert len(progress_calls) >= 2
587588

589+
def test_ip_adapter_loads_embeddings_without_pickle(self):
590+
from opencut.core import character_consistency
591+
592+
fake_diffusers = types.ModuleType("diffusers")
593+
594+
class DummyPipeline:
595+
@classmethod
596+
def from_pretrained(cls, *_args, **_kwargs):
597+
return cls()
598+
599+
def to(self, _device):
600+
return self
601+
602+
fake_diffusers.StableDiffusionPipeline = DummyPipeline
603+
fake_torch = types.ModuleType("torch")
604+
fake_torch.float16 = object()
605+
fake_torch.cuda = types.SimpleNamespace(is_available=lambda: False)
606+
607+
with patch.dict(sys.modules, {"diffusers": fake_diffusers, "torch": fake_torch}), \
608+
patch("opencut.core.character_consistency.ensure_package", return_value=True), \
609+
patch("numpy.load", return_value={}) as load_mock:
610+
result = character_consistency._generate_with_ip_adapter(
611+
"hero in a city",
612+
"embeddings.npz",
613+
"out.mp4",
614+
1.0,
615+
)
616+
617+
assert result == ""
618+
load_mock.assert_called_once_with("embeddings.npz", allow_pickle=False)
619+
620+
621+
class TestDepthSegmentCompose:
622+
def test_depth_archives_load_without_pickle(self):
623+
from opencut.core import compose_depth_segment
624+
625+
fake_cv2 = types.ModuleType("cv2")
626+
fake_cv2.CAP_PROP_FPS = 5
627+
fake_cv2.CAP_PROP_FRAME_WIDTH = 3
628+
fake_cv2.CAP_PROP_FRAME_HEIGHT = 4
629+
fake_cv2.VideoWriter_fourcc = lambda *_args: 0
630+
631+
class DummyCapture:
632+
def __init__(self, _path):
633+
pass
634+
635+
def get(self, prop):
636+
if prop == fake_cv2.CAP_PROP_FPS:
637+
return 24
638+
if prop == fake_cv2.CAP_PROP_FRAME_WIDTH:
639+
return 1920
640+
if prop == fake_cv2.CAP_PROP_FRAME_HEIGHT:
641+
return 1080
642+
return 0
643+
644+
def read(self):
645+
return False, None
646+
647+
def release(self):
648+
pass
649+
650+
class DummyWriter:
651+
def __init__(self, *_args):
652+
pass
653+
654+
def write(self, _frame):
655+
pass
656+
657+
def release(self):
658+
pass
659+
660+
fake_cv2.VideoCapture = DummyCapture
661+
fake_cv2.VideoWriter = DummyWriter
662+
663+
fake_segment = types.ModuleType("opencut.core.segment_sam2")
664+
fake_segment.segment_video = lambda **_kwargs: types.SimpleNamespace(mask_count=0)
665+
fake_depth = types.ModuleType("opencut.core.depth_anything_v2")
666+
fake_depth.estimate_depth = lambda **_kwargs: types.SimpleNamespace(frames_processed=0)
667+
668+
depth_archive = MagicMock()
669+
depth_archive.__getitem__.return_value = []
670+
671+
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as source:
672+
source.write(b"fake")
673+
video_path = source.name
674+
675+
output_path = ""
676+
try:
677+
with patch.dict(
678+
sys.modules,
679+
{
680+
"cv2": fake_cv2,
681+
"opencut.core.segment_sam2": fake_segment,
682+
"opencut.core.depth_anything_v2": fake_depth,
683+
},
684+
), patch("opencut.core.compose_depth_segment.check_composite_available", return_value=True), \
685+
patch("numpy.load", return_value=depth_archive) as load_mock:
686+
result = compose_depth_segment.compose(
687+
video_path=video_path,
688+
prompts=[{"point": [10, 10]}],
689+
effects=["depth_parallax"],
690+
)
691+
output_path = result.output
692+
693+
assert result.frames_processed == 0
694+
load_mock.assert_called_once()
695+
assert load_mock.call_args.kwargs["allow_pickle"] is False
696+
finally:
697+
os.unlink(video_path)
698+
if output_path and os.path.isfile(output_path):
699+
os.unlink(output_path)
700+
588701

589702
# ===================================================================
590703
# Motion Brush - Dataclasses

0 commit comments

Comments
 (0)