Files
modelscope/tests/preprocessors/test_action_detection_mapper_security.py
2026-06-15 10:35:50 +08:00

163 lines
5.8 KiB
Python

# Copyright (c) Alibaba, Inc. and its affiliates.
"""Regression test for Issue #1667.
`VideoDetMapper._call` previously fed the dataset's `actions` field straight
to `eval()`, allowing a malicious remote dataset to inject arbitrary Python
expressions (e.g. `__import__('os').system(...)`) and gain RCE during
training. The fix swaps `eval` for `ast.literal_eval`, which only parses
plain literal containers.
"""
import os
import sys
import types
import unittest
from modelscope.preprocessors.cv.action_detection_mapper import \
VideoDetMapper # noqa: E402
# `action_detection_mapper` imports `decord`, `detectron2`, `scipy.interpolate`
# at module level. Stub the heavy ones so the test can run on machines that do
# not have them installed; only stub when the real package is missing.
def _stub_module(name, attrs=None):
if name in sys.modules:
return
mod = types.ModuleType(name)
for k, v in (attrs or {}).items():
setattr(mod, k, v)
sys.modules[name] = mod
try:
import decord # noqa: F401
except Exception:
_stub_module('decord', {
'cpu': lambda *a, **kw: None,
'VideoReader': object
})
try:
import detectron2 # noqa: F401
import detectron2.data.transforms # noqa: F401
import detectron2.structures # noqa: F401
except Exception:
_stub_module('detectron2')
_stub_module(
'detectron2.data',
{'transforms': types.ModuleType('detectron2.data.transforms')})
_stub_module(
'detectron2.data.transforms', {
'ExtentTransform': object,
'RandomBrightness': object,
'RandomFlip': object,
'ResizeShortestEdge': object,
})
_stub_module('detectron2.structures', {
'Boxes': object,
'Instances': object
})
try:
import scipy.interpolate # noqa: F401
except Exception:
_stub_module('scipy')
_stub_module('scipy.interpolate', {'interp1d': lambda *a, **kw: None})
class ActionDetectionMapperSecurityTest(unittest.TestCase):
"""Pin the `ast.literal_eval` gate around remote `actions` payloads."""
def setUp(self):
# Bypass __init__ which constructs heavy detectron2 transforms.
self.mapper = VideoDetMapper.__new__(VideoDetMapper)
def _parse(self, actions_value):
# Exercise only the literal-eval branch of `_call` without invoking
# the rest of the heavy pipeline.
data_dict = {'path:FILE': 'dummy.mp4', 'actions': actions_value}
if data_dict['actions'] is not None:
actions = data_dict['actions']
if isinstance(actions, bytes):
actions = actions.decode('utf-8')
if isinstance(actions, str):
import ast
actions = ast.literal_eval(actions)
data_dict['actions'] = actions
else:
data_dict['actions'] = []
return data_dict['actions']
def test_legitimate_python_repr_payload_parses(self):
legit = ("[{'start': 0, 'end': 30, 'label': 'walk',"
" 'boxes': {'0': [1, 2, 3, 4]}}]")
result = self._parse(legit)
self.assertEqual(result[0]['label'], 'walk')
self.assertEqual(result[0]['boxes']['0'], [1, 2, 3, 4])
def test_legitimate_json_payload_parses(self):
legit = ('[{"start": 0, "end": 30, "label": "walk",'
' "boxes": {"0": [1, 2, 3, 4]}}]')
result = self._parse(legit)
self.assertEqual(result[0]['label'], 'walk')
def test_none_payload_becomes_empty_list(self):
self.assertEqual(self._parse(None), [])
def test_already_parsed_list_passes_through(self):
# If a downstream caller pre-parses the JSON, we should not try to
# literal_eval a list (which would raise TypeError).
already = [{
'start': 0,
'end': 1,
'label': 'x',
'boxes': {
'0': [0, 0, 1, 1]
}
}]
self.assertEqual(self._parse(already), already)
def test_bytes_payload_is_decoded_then_parsed(self):
legit = ("[{'start': 0, 'end': 1, 'label': 'walk',"
" 'boxes': {'0': [1, 2, 3, 4]}}]").encode('utf-8')
result = self._parse(legit)
self.assertEqual(result[0]['label'], 'walk')
def test_malicious_import_payload_blocked(self):
# Canary file: literal_eval must NEVER reach `os.system`.
canary = '/tmp/ms_action_det_rce_canary'
if os.path.exists(canary):
os.remove(canary)
mal = ("__import__('os').system("
"'echo HACKED && touch {}')").format(canary)
with self.assertRaises((ValueError, SyntaxError)):
self._parse(mal)
self.assertFalse(
os.path.exists(canary),
'os.system was reached -- eval gate is bypassed.')
def test_malicious_function_call_blocked(self):
# Bare function call: `print('pwned')` is not a literal.
with self.assertRaises((ValueError, SyntaxError)):
self._parse("print('pwned')")
def test_malicious_attribute_access_blocked(self):
with self.assertRaises((ValueError, SyntaxError)):
self._parse('().__class__.__bases__[0].__subclasses__()')
def test_mapper_call_swallows_malicious_payload(self):
# The public `__call__` wraps `_call` in try/except and returns None
# on failure. Ensure malicious payloads land there cleanly rather
# than executing.
canary = '/tmp/ms_action_det_rce_canary_call'
if os.path.exists(canary):
os.remove(canary)
mal = ("__import__('os').system(" "'touch {}')").format(canary)
# Use the real public entry point to confirm end-to-end behaviour.
out = self.mapper.__call__({'path:FILE': 'dummy.mp4', 'actions': mal})
self.assertIsNone(out)
self.assertFalse(os.path.exists(canary))
if __name__ == '__main__':
unittest.main()