mirror of
https://github.com/modelscope/modelscope.git
synced 2026-09-01 19:49:03 +02:00
163 lines
5.8 KiB
Python
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()
|