WeKnora 命令注入漏洞 | CVE-2026-22688 复现&研究
WeKnora是一款基于大语言模型(LLM)的框架,专为深度文档理解与语义检索而设计。在0.2.5之前的版本中,存在一个命令注入漏洞,允许经过身份验证的用户向MCP标准输入输出设置中注入stdio_config.command/args参数,导致服务器使用这些注入值执行子进程。该漏洞已在0.2.5版本中修复。
0x0 背景介绍
WeKnora是一款基于大语言模型(LLM)的框架,专为深度文档理解与语义检索而设计。在0.2.5之前的版本中,存在一个命令注入漏洞,允许经过身份验证的用户向MCP标准输入输出设置中注入stdio_config.command/args参数,导致服务器使用这些注入值执行子进程。该漏洞已在0.2.5版本中修复。
0x1 环境搭建
1.1、Ubuntu24+Docker搭建配置
#1、创建专属项目
mkdir WeKnora214 && cd WeKnora214/
#2、拉取WeKnora环境
git clone https://github.com/Tencent/WeKnora.git
cd WeKnora
#2.1使用指定版本
git checkout v0.2.4
#2.2检查版本
git describe --tags
输出:v0.2.4
#3、编辑yml,原始中均为latest,需改为0.2.4版本
cat docker-compose.yml | grep wechat
image: wechatopenai/weknora-ui:v0.2.4
image: wechatopenai/weknora-app:v0.2.4
image: wechatopenai/weknora-docreader:v0.2.4
#4、创建env
# ========== 基础配置 ==========
GIN_MODE=release
DISABLE_REGISTRATION=false
# ========== 数据库 ==========
DB_DRIVER=postgres
DB_USER=weknora
DB_PASSWORD=WeKnora@2026!SecurePass
DB_NAME=weknora
# ========== 向量与存储 ==========
RETRIEVE_DRIVER=postgres
STORAGE_TYPE=local
LOCAL_STORAGE_BASE_DIR=/data/files
AUTO_RECOVER_DIRTY=true
# ========== Redis 流管理 ==========
STREAM_MANAGER_TYPE=redis
REDIS_PASSWORD=Redis@2026!StreamPass
REDIS_DB=0
REDIS_PREFIX=weknora:
# ========== 安全密钥(已预生成,建议保留或自行替换)==========
JWT_SECRET=9f3b1a8c5e7d2f6a4c0b9e1d8f3a7c5b2e6d9f0a1c4b8e3d7f2a5c9b6e0d4f1a
TENANT_AES_KEY=k3x9LmQpR7vYzN2sA5tG8wE1uH4jC6bF
# ========== Ollama ==========
OLLAMA_BASE_URL=http://host.docker.internal:11434
# ========== 并发控制 ==========
CONCURRENCY_POOL_SIZE=3
# ========== 网络与端口 ==========
APP_PORT=8080
FRONTEND_PORT=80
DOCREADER_PORT=50051
# ========== 腾讯云 COS(未启用,保持默认)==========
COS_ENABLE_OLD_DOMAIN=true
# ========== 图谱(禁用)==========
ENABLE_GRAPH_RAG=false
NEO4J_ENABLE=false
# ========== MinIO(如需启用,在 docker-compose 中使用 --profile minio)==========
MINIO_ACCESS_KEY_ID=minioadmin
MINIO_SECRET_ACCESS_KEY=miniostrongpassword2026
# ========== 其他 ==========
APK_MIRROR_ARG=mirrors.tencent.com
#4、拉取镜像
docker compose --profile minio up -d
#5、查看启动情况
docker ps
#6、观察日志
docker logs -f WeKnora-app
#7、无误后web访问127.0.0.1进行注册登录

0x2 漏洞复现
2.1、Python检查
https://github.com/Kai-One001/cve-/blob/main/WeKnora_CVE-2026-22688.py

2.2、手动复现步骤
- 标准创建流程
- 创建流程

- 访问
test激活,也可以web中直接测试(用户->MCP服务)
- 验证存在

2.3、复现流量特征 (PCAP)
- 获取
TOKEN和创建流程
- 激活流程

- 验证成功

0x3 漏洞原理分析
3.1、入口文件分析
PS:照例根据公开的路由尝试再IntelliJ IDEA中查询文件,排除MD后定位到WeKnora-0.2.4\WeKnora-0.2.4\frontend\src\api\mcp-service.ts
import { get, post, put, del } from '@/utils/request'
export interface MCPService {
id: string
tenant_id?: number
name: string
description: string
enabled: boolean
transport_type: 'sse' | 'http-streamable' | 'stdio'
url?: string // Optional: required for SSE/HTTP Streamable
headers?: Record<string, string>
auth_config?: {
api_key?: string
token?: string
custom_headers?: Record<string, string>
}
advanced_config?: {
timeout?: number
retry_count?: number
retry_delay?: number
}
stdio_config?: {
command: 'uvx' | 'npx' // Command: uvx or npx
args: string[] // Command arguments array
}
env_vars?: Record<string, string> // Environment variables for stdio transport
created_at?: string
updated_at?: string
}
export interface MCPTool {
name: string
description: string
inputSchema: Record<string, any>
}
export interface MCPResource {
uri: string
name: string
description?: string
mimeType?: string
}
export interface MCPTestResult {
success: boolean
message?: string
tools?: MCPTool[]
resources?: MCPResource[]
}
// List all MCP services
export async function listMCPServices(): Promise<MCPService[]> {
const response: any = await get('/api/v1/mcp-services')
return response.data || []
}
// Get a single MCP service by ID
export async function getMCPService(id: string): Promise<MCPService> {
const response: any = await get(`/api/v1/mcp-services/${id}`)
return response.data
}
// Create a new MCP service
export async function createMCPService(data: Partial<MCPService>): Promise<MCPService> {
const response: any = await post('/api/v1/mcp-services', data)
return response.data
}
// Update an existing MCP service
export async function updateMCPService(id: string, data: Partial<MCPService>): Promise<MCPService> {
const response: any = await put(`/api/v1/mcp-services/${id}`, data)
return response.data
}
// Delete an MCP service
export async function deleteMCPService(id: string): Promise<void> {
await del(`/api/v1/mcp-services/${id}`)
}
// Test MCP service connection
export async function testMCPService(id: string): Promise<MCPTestResult> {
const response: any = await post(`/api/v1/mcp-services/${id}/test`, {})
// 后端返回格式: { success: true, data: MCPTestResult }
// response interceptor 已经返回了 data,所以 response 就是 { success: true, data: {...} }
if (response && response.data) {
return response.data
}
// 如果格式不对,尝试直接返回 response(可能是直接返回的数据)
return response
}
// Get tools from an MCP service
export async function getMCPServiceTools(id: string): Promise<MCPTool[]> {
const response: any = await get(`/api/v1/mcp-services/${id}/tools`)
return response.data || []
}
// Get resources from an MCP service
export async function getMCPServiceResources(id: string): Promise<MCPResource[]> {
const response: any = await get(`/api/v1/mcp-services/${id}/resources`)
return response.data || []
}
- 虽然前端
TypeScript类型声明限制command 为 'uvx' | 'npx',但后端Go服务完全未做校验,且HTTP请求是原始JSON,攻击者可直接发送请求 - 参数是
Partial,说明可部分更新updateMCPService(id: string, data: Partial<MCPService>) - 所有相关
API接口
| 动作(Action) | HTTP 方法 | URL | 示例说明 |
|---|---|---|---|
| createMCPService | POST | /api/v1/mcp-services/ | 创建新 MCP 服务(支持 stdio_config.command 任意值) |
| updateMCPService | PUT | /api/v1/mcp-services/{id} | 更新现有 MCP 服务(可单独修改 command 字段) |
| testMCPService | POST | /api/v1/mcp-services/{id}/test | 测试 MCP 服务连接并获取工具列表 |
| getMCPServiceTools | GET | /api/v1/mcp-services/{id}/tools | 获取该 MCP 服务声明的工具列表(需服务已启用且连接正常) |
3.2、路由注册
// 需要认证的API路由
v1 := r.Group("/api/v1")
{
RegisterAuthRoutes(v1, params.AuthHandler)
RegisterTenantRoutes(v1, params.TenantHandler)
RegisterKnowledgeBaseRoutes(v1, params.KBHandler)
RegisterKnowledgeTagRoutes(v1, params.TagHandler)
RegisterKnowledgeRoutes(v1, params.KnowledgeHandler)
RegisterFAQRoutes(v1, params.FAQHandler)
RegisterChunkRoutes(v1, params.ChunkHandler)
RegisterSessionRoutes(v1, params.SessionHandler)
RegisterChatRoutes(v1, params.SessionHandler)
RegisterMessageRoutes(v1, params.MessageHandler)
RegisterModelRoutes(v1, params.ModelHandler)
RegisterEvaluationRoutes(v1, params.EvaluationHandler)
RegisterInitializationRoutes(v1, params.InitializationHandler)
RegisterSystemRoutes(v1, params.SystemHandler)
RegisterMCPServiceRoutes(v1, params.MCPServiceHandler)
RegisterWebSearchRoutes(v1, params.WebSearchHandler)
}
return r
}
/ RegisterMCPServiceRoutes registers MCP service routes
func RegisterMCPServiceRoutes(r *gin.RouterGroup, handler *handler.MCPServiceHandler) {
mcpServices := r.Group("/mcp-services")
{
// Create MCP service
mcpServices.POST("", handler.CreateMCPService)
// List MCP services
mcpServices.GET("", handler.ListMCPServices)
// Get MCP service by ID
mcpServices.GET("/:id", handler.GetMCPService)
// Update MCP service
mcpServices.PUT("/:id", handler.UpdateMCPService)
// Delete MCP service
mcpServices.DELETE("/:id", handler.DeleteMCPService)
// Test MCP service connection
mcpServices.POST("/:id/test", handler.TestMCPService)
// Get MCP service tools
mcpServices.GET("/:id/tools", handler.GetMCPServiceTools)
// Get MCP service resources
mcpServices.GET("/:id/resources", handler.GetMCPServiceResources)
}
}
- 所有
MCP服务操作都自动加前缀/api/v1/mcp-services/...路由暴露
POST /api/v1/mcp-services 请求 → 被 Gin 路由分发 → 调用 handler.MCPServiceHandler.CreateMCPService 方法
在IDE查找CreateMCPService,发现有interfaces\mcp_service.go、handler\mcp_service.go、service\mcp_service.go、mcp-service.ts、McpServiceDialog.vue文件都有,于是查询了信息
| 文件路径 | 语言/类型 | 作用 |
|---|---|---|
| interfaces/mcp_service.go | Go (接口定义) | 定义 MCPService 接口,包含 CreateMCPService 方法签名 |
| handler/mcp_service.go | Go (HTTP Handler) | 实现 Gin 路由对应的业务入口,接收 HTTP 请求 |
| service/mcp_service.go | Go (业务逻辑) | 实现 MCPService 接口,处理数据校验、存储等 |
| mcp-service.ts | TypeScript | 前端 API 封装或类型定义 |
| McpServiceDialog.vue | Vue (前端组件) | 用户在界面上创建 MCP 服务的表单 |
3.3、HTTP Handler 层
- 创建服务
handler/mcp_service.go
// CreateMCPService godoc
// @Summary 创建MCP服务
// @Description 创建新的MCP服务配置
// @Tags MCP服务
// @Accept json
// @Produce json
// @Param request body types.MCPService true "MCP服务配置"
// @Success 200 {object} map[string]interface{} "创建的MCP服务"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Security Bearer
// @Router /mcp-services [post]
func (h *MCPServiceHandler) CreateMCPService(c *gin.Context) {
ctx := c.Request.Context()
var service types.MCPService
if err := c.ShouldBindJSON(&service); err != nil {//直接反序列化用户输入
logger.Error(ctx, "Failed to parse MCP service request", err)
c.Error(errors.NewBadRequestError(err.Error()))
return
}
tenantID := c.GetUint64(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
service.TenantID = tenantID
if err := h.mcpServiceService.CreateMCPService(ctx, &service); err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_name": secutils.SanitizeForLog(service.Name)})
c.Error(errors.NewInternalServerError("Failed to create MCP service: " + err.Error()))
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": service,
})
}
CreateMCPService直接绑定用户输入到types.MCPService,如果types.MCPService包含StdioConfig.Command字段 → 则用户完全控制该字段- 更新服务也是同样,允许任意设置
command
func (h *MCPServiceHandler) UpdateMCPService(c *gin.Context) {
。。。。。
if stdioConfig, ok := updateData["stdio_config"].(map[string]interface{}); ok {
config := &types.MCPStdioConfig{}
if command, ok := stdioConfig["command"].(string); ok {
config.Command = command //这里
}
if args, ok := stdioConfig["args"].([]interface{}); ok {
config.Args = make([]string, len(args))
for i, arg := range args {
if str, ok := arg.(string); ok {
config.Args[i] = str
}
}
}
service.StdioConfig = config
}
-
不仅创建时可控,更新时也可修改
command参数 -
攻击者可先创建一个合法服务,再更新为恶意命令
-
TestMCPService会触发命令执行
result, err := h.mcpServiceService.TestMCPService(ctx, tenantID, serviceID)
结合在interfaces/mcp_service.go中的定义
TestMCPService(ctx context.Context, tenantID uint64, id string) (*types.MCPTestResult, error)
“测试连接” 对于stdio类型的MCP服务,启动子进程执行command + args,并尝试通信
3.4、接口定义层(interfaces)
//主要是定义了两类接口
type MCPServiceRepository interface {
Create(ctx context.Context, service *types.MCPService) error
GetByID(ctx context.Context, tenantID uint64, id string) (*types.MCPService, error)
// ... 其他 CRUD 方法
}
- 数据访问层
DAO/Repo的接口 - 负责与数据库交互
//MCPServiceService 接口
type MCPServiceService interface {
CreateMCPService(ctx context.Context, service *types.MCPService) error
GetMCPServiceByID(...)
TestMCPService(...)
GetMCPServiceTools(...)
// ...
}
3.5、业务逻辑层(核心漏洞点)
service/mcp_service.go文件
// CreateMCPService creates a new MCP service
func (s *mcpServiceService) CreateMCPService(ctx context.Context, service *types.MCPService) error {
// Set default advanced config if not provided
if service.AdvancedConfig == nil {
service.AdvancedConfig = types.GetDefaultAdvancedConfig()
}
// Set timestamps
service.CreatedAt = time.Now()
service.UpdatedAt = time.Now()
if err := s.mcpServiceRepo.Create(ctx, service); err != nil {
logger.GetLogger(ctx).Errorf("Failed to create MCP service: %v", err)
return fmt.Errorf("failed to create MCP service: %w", err)
}
return nil
}
-
CreateMCPService—— 无任何校验,原样存储 -
完全信任传入的
*types.MCPService -
未对
service.StdioConfig.Command做任何检查 -
直接调用
repo.Create()存入数据库 -
TestMCPService触发命令执行 -
mcp.NewMCPClient在stdio模式下会启动子进程
client, err := mcp.NewMCPClient(&mcp.ClientConfig{Service: service})
client.Connect(testCtx) // ← 在 stdio 模式下会 exec.Command(service.StdioConfig.Command, ...)
-
UpdateMCPService允许动态覆盖StdioConfig
if service.StdioConfig != nil {
existing.StdioConfig = service.StdioConfig // ← 完全覆盖!
}
其它姿势:
先创建一个合法服务
再通过 PUT /mcp-services/{id} 更新 stdio_config.command 为恶意 payload
点击“测试”触发 RCE
0x4 修复建议
修复方案
- 升级到最新版本:建议受影响的用户升级至
0.2.5或更高:WeKnora - 临时防护措施:
权限最小化:限制普通用户对MCP模块配置项的修改权限,管理员用户进行强密码处理
启用 WAF/IPS 规则:监控异常的system()、subprocess.run()等调用行为,及时发现潜在攻击。
加强输入验证:涉及stdio_config.command和args参数,实施严格的白名单校验与转义处理
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。
更多推荐


所有评论(0)