fix(security): replace episode offset eval with safe arithmetic

This commit is contained in:
jxxghp
2026-09-11 16:59:51 +08:00
parent 643efea1aa
commit 65b9c777de
5 changed files with 67 additions and 24 deletions

View File

@@ -3,11 +3,12 @@ from collections import defaultdict
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Dict, Iterable, List, Match, Optional, Tuple, Union
from typing import Dict, Iterable, List, Match, Optional, Tuple, Union, cast
import anitopy
from app.application.configuration import get_chain_runtime_config_snapshot
from app.domain.meta.words import calculate_episode_offset
from app.domain.metainfo import MetaInfoPath
from app.domain.meta.metabase import MetaBase
from app.runtime.log import logger
@@ -178,31 +179,28 @@ class FormatParser(object):
if isinstance(self._start_ep, str):
# `details` 格式为 `X-X`
s, e = self._start_ep.split("-")
start_ep = self.__offset.replace("EP", s)
end_ep = self.__offset.replace("EP", e)
if int(s) == int(e):
return int(eval(start_ep)), None, self.part
return int(eval(start_ep)), int(eval(end_ep)), self.part
return calculate_episode_offset(self.__offset, int(s)), None, self.part
return calculate_episode_offset(self.__offset, int(s)), calculate_episode_offset(self.__offset, int(e)), self.part
else:
# `details` 格式为 `X`
start_ep = self.__offset.replace("EP", str(self._start_ep))
return int(eval(start_ep)), None, self.part
return calculate_episode_offset(self.__offset, self._start_ep), None, self.part
elif not self._format:
# `details` 格式为 `X,X`
start_ep = self.__offset.replace("EP", str(self._start_ep))
end_ep = self.__offset.replace("EP", str(self._end_ep))
return int(eval(start_ep)), int(eval(end_ep)), self.part
start_ep: Optional[int] = calculate_episode_offset(self.__offset, cast(int, self._start_ep))
end_ep: Optional[int] = calculate_episode_offset(self.__offset, cast(int, self._end_ep))
return start_ep, end_ep, self.part
if not self._format:
# 未填入`集数定位` 且没有`指定集数` 仅处理`集数偏移`
start_ep = eval(self.__offset.replace("EP", str(file_meta.begin_episode))) if file_meta.begin_episode else None
end_ep = eval(self.__offset.replace("EP", str(file_meta.end_episode))) if file_meta.end_episode else None
return int(start_ep) if start_ep else None, int(end_ep) if end_ep else None, self.part
start_ep = calculate_episode_offset(self.__offset, file_meta.begin_episode) if file_meta.begin_episode else None
end_ep = calculate_episode_offset(self.__offset, file_meta.end_episode) if file_meta.end_episode else None
return start_ep if start_ep else None, end_ep if end_ep else None, self.part
else:
# 有`集数定位`
s, e = self.__handle_single(file_name)
start_ep = self.__offset.replace("EP", str(s)) if s else None
end_ep = self.__offset.replace("EP", str(e)) if e else None
return int(eval(start_ep)) if start_ep else None, int(eval(end_ep)) if end_ep else None, self.part
matched_start, matched_end = self.__handle_single(file_name)
start_ep = calculate_episode_offset(self.__offset, matched_start) if matched_start else None
end_ep = calculate_episode_offset(self.__offset, matched_end) if matched_end else None
return start_ep if start_ep else None, end_ep if end_ep else None, self.part
def __handle_single(self, file: str) -> Tuple[Optional[int], Optional[int]]:
"""

View File

@@ -51,7 +51,7 @@ def _compile_custom_word_regex(pattern: str):
return re.compile(pattern)
def _calculate_episode_offset(offset: str, episode: int) -> int:
def calculate_episode_offset(offset: str, episode: int) -> int:
"""
按白名单算术语法计算集数偏移,避免执行任意表达式。
"""
@@ -192,7 +192,7 @@ class WordsMatcher(metaclass=Singleton):
offset_order_flag = False
for episode_num_str in episode_nums_str:
episode_num_int = int(cn2an.cn2an(episode_num_str, "smart"))
episode_num_offset_int = _calculate_episode_offset(offset, episode_num_int)
episode_num_offset_int = calculate_episode_offset(offset, episode_num_int)
# 向前偏移
if episode_num_int > episode_num_offset_int:
offset_order_flag = True

View File

@@ -1077,8 +1077,8 @@
"runtime_only": true
}
},
"edge_count": 8551,
"edge_sha256": "3a52844425d3330b00935d7d565e468506a85ae62972b5c257b409b5b000206b",
"edge_count": 8552,
"edge_sha256": "750c2b2d5ff50ba8c004011261f52e79bf1978cbaeac7019522cea67e073ca98",
"edges": [
"app -> app.foundation",
"app -> app.foundation.environment",
@@ -3356,6 +3356,7 @@
"app.application.formatting -> app.domain",
"app.application.formatting -> app.domain.meta",
"app.application.formatting -> app.domain.meta.metabase",
"app.application.formatting -> app.domain.meta.words",
"app.application.formatting -> app.domain.metainfo",
"app.application.formatting -> app.runtime",
"app.application.formatting -> app.runtime.log",

View File

@@ -630,7 +630,7 @@
},
"app/application/formatting.py": {
"arg-type": 4,
"assignment": 9,
"assignment": 4,
"attr-defined": 1,
"no-untyped-def": 7,
"operator": 3,

View File

@@ -1,11 +1,13 @@
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from app.chain.transfer import TransferChain
from app.application.formatting import EpisodeFormatRuleHelper, FormatParser, _AutoRecommendSample
from app.schemas import EpisodeFormatRule, FileItem
from app.chain.transfer.facade import TransferChain
from app.schemas.file import FileItem
from app.schemas.transfer import EpisodeFormatRule
def _make_file(name: str, size: int = 150 * 1024 * 1024) -> FileItem:
@@ -32,6 +34,48 @@ def _patch_media_exts(monkeypatch):
)
@pytest.mark.parametrize(
("offset", "expected"),
[("-1", 2), ("EP*2-1", 5), ("EP+EP-1", 5)],
)
def test_format_parser_applies_safe_episode_offset(offset: str, expected: int):
"""剧集偏移仅计算白名单算术表达式,并保留现有偏移语义。"""
parser = FormatParser(eformat="", offset=offset)
file_meta = SimpleNamespace(begin_episode=3, end_episode=3)
assert parser.split_episode("", file_meta) == (expected, expected, None)
def test_format_parser_applies_safe_episode_offset_to_template_episode():
"""带定位模板时也应使用白名单计算集数偏移。"""
parser = FormatParser(eformat="Show - {ep}.mkv", offset="EP*2-1")
file_meta = SimpleNamespace(begin_episode=None, end_episode=None)
assert parser.split_episode("Show - 03.mkv", file_meta) == (5, None, None)
def test_format_parser_applies_safe_episode_offset_to_explicit_range():
"""显式集数区间的起止集数都应使用安全偏移计算。"""
parser = FormatParser(eformat="", details="03-04", offset="EP+1")
file_meta = SimpleNamespace(begin_episode=None, end_episode=None)
assert parser.split_episode("", file_meta) == (4, 5, None)
def test_format_parser_rejects_code_execution_in_episode_offset():
"""剧集偏移中的函数调用必须被拒绝,不能执行系统命令。"""
parser = FormatParser(
eformat="",
offset="EP+__import__('os').system('touch /tmp/moviepilot-format-parser-rce')*0",
)
file_meta = SimpleNamespace(begin_episode=1, end_episode=1)
with patch("os.system") as system_call, pytest.raises(ValueError):
parser.split_episode("", file_meta)
system_call.assert_not_called()
def test_rule_recommend_supports_range_episode_validation():
helper = EpisodeFormatRuleHelper()
rule = EpisodeFormatRule(