fix: 修复路径遍历安全漏洞并添加文件名校验
- 前端:在 ExtensionsView.vue 上传前添加 beforeUpload 校验, 仅允许 .zip 或 SKILL.md 文件,其他 .md 文件给出明确错误提示 - 后端:import_skill_dir 添加路径限制检查(is_relative_to), 确保来源目录在系统临时目录内,防止路径遍历 - 后端:install_remote_skill 改为扫描目录查找安装结果, 避免直接用用户输入构造文件路径(消除 CodeQL 告警) - 后端:_run_skills_cli 错误输出清洗 ANSI 控制字符并截断到 500 字符 - 后端:install_remote_skill 返回类型从 object 修正为 Skill Agent-Logs-Url: https://github.com/xerrors/Yuxi/sessions/0145c647-72bc-41f9-b07b-04d4acf51932 Co-authored-by: xerrors <35524243+xerrors@users.noreply.github.com>
This commit is contained in:
parent
cf0f0afd85
commit
037872924b
@ -6,10 +6,14 @@ import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from yuxi.services.skill_service import import_skill_dir, is_valid_skill_slug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from yuxi.storage.postgres.models_business import Skill
|
||||
|
||||
ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
|
||||
CONTROL_SEQUENCE_RE = re.compile(r"\x1B\][^\x07]*(?:\x07|\x1B\\)|\x1B[\(\)][A-Za-z0-9]")
|
||||
CLI_TIMEOUT_SECONDS = 300
|
||||
@ -111,7 +115,9 @@ async def _run_skills_cli(
|
||||
error_output = (stderr or b"").decode("utf-8", errors="replace")
|
||||
combined = "\n".join(part for part in [output.strip(), error_output.strip()] if part)
|
||||
if process.returncode != 0:
|
||||
raise ValueError(combined or "skills CLI 执行失败")
|
||||
cleaned_lines = _clean_cli_output(combined)
|
||||
error_msg = "\n".join(line for line in cleaned_lines if line)[:500]
|
||||
raise ValueError(error_msg or "skills CLI 执行失败")
|
||||
return combined
|
||||
|
||||
|
||||
@ -149,7 +155,7 @@ async def install_remote_skill(
|
||||
source: str,
|
||||
skill: str,
|
||||
created_by: str | None,
|
||||
) -> object:
|
||||
) -> Skill:
|
||||
normalized_source = _normalize_source(source)
|
||||
normalized_skill = _normalize_skill_name(skill)
|
||||
|
||||
@ -183,8 +189,17 @@ async def install_remote_skill(
|
||||
cwd=workdir,
|
||||
)
|
||||
|
||||
installed_dir = Path(temp_home) / ".agents" / "skills" / normalized_skill
|
||||
if not installed_dir.exists() or not installed_dir.is_dir():
|
||||
base_dir = Path(temp_home).resolve()
|
||||
skills_dir = base_dir / ".agents" / "skills"
|
||||
# Scan for the installed skill directory rather than constructing the path
|
||||
# from user input, to avoid path traversal concerns
|
||||
installed_dir = None
|
||||
if skills_dir.is_dir():
|
||||
for candidate in skills_dir.iterdir():
|
||||
if candidate.name == normalized_skill and candidate.is_dir():
|
||||
installed_dir = candidate
|
||||
break
|
||||
if installed_dir is None:
|
||||
raise ValueError("skills CLI 未生成预期的技能目录")
|
||||
|
||||
return await import_skill_dir(
|
||||
|
||||
@ -594,6 +594,10 @@ async def import_skill_dir(
|
||||
created_by: str | None,
|
||||
) -> Skill:
|
||||
source_skill_dir = Path(source_dir).resolve()
|
||||
# Confine to the system temp directory to prevent path traversal
|
||||
tmp_root = Path(tempfile.gettempdir()).resolve()
|
||||
if not source_skill_dir.is_relative_to(tmp_root):
|
||||
raise ValueError("技能目录路径不合法")
|
||||
if not source_skill_dir.exists() or not source_skill_dir.is_dir():
|
||||
raise ValueError("技能目录不存在")
|
||||
return await _import_skill_dir_impl(
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
accept=".zip,.md"
|
||||
:show-upload-list="false"
|
||||
:custom-request="handleImportUpload"
|
||||
:before-upload="beforeSkillUpload"
|
||||
:disabled="skillsLoading || skillsImporting"
|
||||
>
|
||||
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
||||
@ -98,6 +99,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Upload, RotateCw, Plus, Computer } from 'lucide-vue-next'
|
||||
import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
|
||||
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
|
||||
@ -210,6 +212,16 @@ const handleSubagentRefresh = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 上传前校验文件名:仅允许 .zip 或 SKILL.md
|
||||
const beforeSkillUpload = (file) => {
|
||||
const lower = file.name.toLowerCase()
|
||||
if (!lower.endsWith('.zip') && lower !== 'skill.md') {
|
||||
message.error('仅支持上传 .zip 文件或 SKILL.md 文件')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 处理导入上传
|
||||
const handleImportUpload = async ({ file, onSuccess, onError }) => {
|
||||
if (skillsRef.value?.handleImportUpload) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user