diff --git a/.gitignore b/.gitignore index 726fabc..859e3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ __pycache__/ *.pyo *.pyd -# Dependencies -node_modules/ - # Logs and temp files *.log *.tmp @@ -17,12 +14,32 @@ node_modules/ .env.local *.env.* -# Editors -.vscode/ -.idea/ +# Dependencies +.venv/ +venv/ +node_modules/ # Build artifacts dist/ build/ target/ + +# Coverage +.coverage +coverage/ +htmlcov/ + +# Editors +.vscode/ +.idea/ + +# System +.DS_Store +Thumbs.db + +# MyPy +.mypy_cache/ + +# Pytest +.pytest_cache/ ``` \ No newline at end of file diff --git a/oss/shared/__pycache__/router.cpython-312.pyc b/oss/shared/__pycache__/router.cpython-312.pyc index c332a70..d7976bb 100644 Binary files a/oss/shared/__pycache__/router.cpython-312.pyc and b/oss/shared/__pycache__/router.cpython-312.pyc differ diff --git a/store/@{Falck}/html-render/__pycache__/main.cpython-312.pyc b/store/@{Falck}/html-render/__pycache__/main.cpython-312.pyc index 353bd94..6e7f339 100644 Binary files a/store/@{Falck}/html-render/__pycache__/main.cpython-312.pyc and b/store/@{Falck}/html-render/__pycache__/main.cpython-312.pyc differ diff --git a/store/@{FutureOSS}/log-terminal/__pycache__/main.cpython-312.pyc b/store/@{FutureOSS}/log-terminal/__pycache__/main.cpython-312.pyc index a288565..9cd9557 100644 Binary files a/store/@{FutureOSS}/log-terminal/__pycache__/main.cpython-312.pyc and b/store/@{FutureOSS}/log-terminal/__pycache__/main.cpython-312.pyc differ diff --git a/store/@{FutureOSS}/pkg-manager/__pycache__/main.cpython-312.pyc b/store/@{FutureOSS}/pkg-manager/__pycache__/main.cpython-312.pyc index eac64e7..5c5b9ad 100644 Binary files a/store/@{FutureOSS}/pkg-manager/__pycache__/main.cpython-312.pyc and b/store/@{FutureOSS}/pkg-manager/__pycache__/main.cpython-312.pyc differ diff --git a/store/@{FutureOSS}/pkg-manager/main.py b/store/@{FutureOSS}/pkg-manager/main.py index 70f3022..15b0b08 100644 --- a/store/@{FutureOSS}/pkg-manager/main.py +++ b/store/@{FutureOSS}/pkg-manager/main.py @@ -2,6 +2,7 @@ import os import sys import json +import html import urllib.request from pathlib import Path from oss.logger.logger import Log @@ -112,15 +113,19 @@ class PkgManagerPlugin(Plugin): for pkg_name, info in plugins.items(): status_class = "success" if info.get('enabled', False) else "secondary" status_text = "已启用" if info.get('enabled', False) else "已禁用" + # XSS 防护:对所有用户数据进行 HTML 转义 + safe_pkg_name = html.escape(pkg_name) + safe_version = html.escape(str(info.get('version', '未知'))) + safe_author = html.escape(str(info.get('author', '未知'))) plugin_rows += f""" - {pkg_name} - {info.get('version', '未知')} - {info.get('author', '未知')} + {safe_pkg_name} + {safe_version} + {safe_author} {status_text} - - + + """ @@ -209,15 +214,23 @@ class PkgManagerPlugin(Plugin): plugin_cards = "" for pkg_name, info in available.items(): is_installed = pkg_name in installed - action_btn = f'' if not is_installed else '' + # XSS 防护:对所有用户数据进行 HTML 转义 + safe_pkg_name = html.escape(pkg_name) + safe_name = html.escape(str(info.get('name', pkg_name))) + safe_desc = html.escape(str(info.get('description', '暂无描述'))) + safe_version = html.escape(str(info.get('version', '未知'))) + safe_author = html.escape(str(info.get('author', '未知'))) + # JavaScript 中的字符串也需要转义 + js_safe_pkg_name = pkg_name.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"') + action_btn = f'' if not is_installed else '' plugin_cards += f"""
-

{info.get('name', pkg_name)}

-

{info.get('description', '暂无描述')}

+

{safe_name}

+

{safe_desc}

- 版本:{info.get('version', '未知')} - 作者:{info.get('author', '未知')} + 版本:{safe_version} + 作者:{safe_author}
{action_btn} diff --git a/store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-312.pyc b/store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-312.pyc index d140b05..82e2187 100644 Binary files a/store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-312.pyc and b/store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-312.pyc differ diff --git a/store/@{FutureOSS}/plugin-loader/main.py b/store/@{FutureOSS}/plugin-loader/main.py index 2ac5ece..ca7600d 100644 --- a/store/@{FutureOSS}/plugin-loader/main.py +++ b/store/@{FutureOSS}/plugin-loader/main.py @@ -213,16 +213,48 @@ class PLInjector: return False def _static_source_check(self, source: str, file_path: str): - """静态源码安全检查""" + """静态源码安全检查 - 增强版,防止字符串拼接/编码绕过""" + import base64 + + # 首先检查是否有 base64 编码的恶意代码 + try: + # 查找所有字符串字面量 + string_pattern = r'([A-Za-z0-9+/=]{20,})' + for match in re.finditer(string_pattern, source): + try: + decoded = base64.b64decode(match.group(1)).decode('utf-8', errors='ignore') + # 检查解码后的内容 + for dangerous in ['import ', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess']: + if dangerous in decoded: + raise PLValidationError(f"{file_path} - 检测到 base64 编码的恶意代码") + except: + pass + except: + pass + + # 检查字符串拼接绕过 (如 'ex' + 'ec') + concat_patterns = [ + r"""['"]ex['"]\s*\+\s*['"]ec['"]""", + r"""['"]impor['"]\s*\+\s*['"]t['"]""", + r"""['"]eva['"]\s*\+\s*['"]l['"]""", + r"""['"]compil['"]\s*\+\s*['"]e['"]""", + ] + for pattern in concat_patterns: + if re.search(pattern, source): + raise PLValidationError(f"{file_path} - 检测到字符串拼接绕过尝试") + forbidden = [ (r'^\s*import\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)', '禁止导入系统级模块'), (r'^\s*from\s+(os|sys|subprocess|shutil|socket|ctypes|cffi|multiprocessing|threading)\s+import', '禁止导入系统级模块'), (r'__import__\s*\(', '禁止使用 __import__'), - (r'exec\s*\(', '禁止使用 exec'), - (r'eval\s*\(', '禁止使用 eval'), - (r'compile\s*\(', '禁止使用 compile'), - (r'open\s*\(', '禁止直接操作文件'), + (r'(? dict: - """加载插件配置文件""" + """加载插件配置文件 - 使用 ast.literal_eval 安全解析""" + import ast cf = plugin_dir / "config.py" if not cf.exists(): return {} @@ -396,43 +429,86 @@ class PluginManager: Log.error("plugin-loader", f"配置文件编码错误:{cf} - {e}") return {} - # 安全检查 - for p in ['import ', 'open(', 'exec(', 'eval(', 'os.', 'sys.', 'subprocess']: + # 严格检查:不允许任何代码执行 + for p in ['import ', 'from ', 'open(', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess', 'lambda', 'def ', 'class ']: if p in content: Log.warn("plugin-loader", f"{cf} 包含危险代码:{p}") return {} - sg = {"__builtins__": {"True": True, "False": False, "None": None, "dict": dict, "list": list, "str": str, "int": int, "float": float, "bool": bool}} - lv = {} + # 尝试使用 ast.literal_eval 安全解析 try: - code = compile(content, str(cf), "exec") - exec(code, sg, lv) - except SyntaxError as e: - Log.error("plugin-loader", f"配置文件语法错误:{cf} - {e}") - return {} - except NameError as e: - Log.error("plugin-loader", f"配置文件名称错误:{cf} - {e}") - return {} - except TypeError as e: - Log.error("plugin-loader", f"配置文件类型错误:{cf} - {e}") - return {} - except Exception as e: - Log.error("plugin-loader", f"配置文件解析失败:{cf} - {type(e).__name__}: {e}") - return {} + result = ast.literal_eval(content) + if isinstance(result, dict): + return {k: v for k, v in result.items() if not k.startswith("_")} + except (ValueError, SyntaxError): + pass - return {k: v for k, v in lv.items() if not k.startswith("_") and not callable(v)} + # 如果失败,尝试提取简单的键值对 + config = {} + for line in content.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line) + if match: + key, value_str = match.groups() + if key.startswith('_'): + continue + try: + value = ast.literal_eval(value_str) + config[key] = value + except (ValueError, SyntaxError): + Log.warn("plugin-loader", f"{cf} 跳过无效的值:{line}") + continue + return config + def _load_extensions(self, plugin_dir: Path) -> dict: + """加载插件扩展配置 - 使用 ast.literal_eval 安全解析""" + import ast ef = plugin_dir / "extensions.py" - if not ef.exists(): return {} - with open(ef, "r", encoding="utf-8") as f: content = f.read() - for p in ['import ', 'open(', 'exec(', 'eval(', 'os.', 'sys.', 'subprocess']: - if p in content: Log.warn("plugin-loader", f"{ef} 包含危险代码: {p}"); return {} - sg = {"__builtins__": {"True": True, "False": False, "None": None, "dict": dict, "list": list, "str": str, "int": int, "float": float, "bool": bool}} - lv = {} - try: code = compile(content, str(ef), "exec"); exec(code, sg, lv) - except Exception as e: Log.error("plugin-loader", f"扩展文件解析失败: {e}"); return {} - return {k: v for k, v in lv.items() if not k.startswith("_") and not callable(v)} + if not ef.exists(): + return {} + try: + with open(ef, "r", encoding="utf-8") as f: + content = f.read() + except Exception as e: + Log.error("plugin-loader", f"扩展文件读取失败:{e}") + return {} + + # 严格检查:不允许任何代码执行 + for p in ['import ', 'from ', 'open(', 'exec(', 'eval(', 'compile(', 'os.', 'sys.', 'subprocess', 'lambda', 'def ', 'class ']: + if p in content: + Log.warn("plugin-loader", f"{ef} 包含危险代码:{p}") + return {} + + # 尝试使用 ast.literal_eval 安全解析 + try: + result = ast.literal_eval(content) + if isinstance(result, dict): + return {k: v for k, v in result.items() if not k.startswith("_")} + except (ValueError, SyntaxError): + pass + + # 如果失败,尝试提取简单的键值对 + extensions = {} + for line in content.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', line) + if match: + key, value_str = match.groups() + if key.startswith('_'): + continue + try: + value = ast.literal_eval(value_str) + extensions[key] = value + except (ValueError, SyntaxError): + Log.warn("plugin-loader", f"{ef} 跳过无效的值:{line}") + continue + return extensions + def load(self, plugin_dir: Path, use_sandbox: bool = True) -> Optional[Any]: """加载单个插件"""