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:
copilot-swe-agent[bot] 2026-04-02 15:19:33 +00:00 committed by GitHub
parent cf0f0afd85
commit 037872924b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 35 additions and 4 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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) {