|
| 1 | +package mcp |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "regexp" |
| 7 | + "strings" |
| 8 | + |
| 9 | + "github.com/modelcontextprotocol/go-sdk/mcp" |
| 10 | + k8stypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types" |
| 11 | +) |
| 12 | + |
| 13 | +const defaultMaxLines = 100 |
| 14 | + |
| 15 | +func (s *MCPServer) runCommand(ctx context.Context, req *mcp.CallToolRequest, namespacedName k8stypes.NamespacedNameParams, |
| 16 | + commands []string) ([]string, error) { |
| 17 | + _, result, err := s.k8sMcpServer.ExecPod(ctx, req, k8stypes.ExecPodParams{NamespacedNameParams: namespacedName, Command: commands}) |
| 18 | + if err != nil { |
| 19 | + return nil, err |
| 20 | + } |
| 21 | + if result.Stderr != "" { |
| 22 | + return nil, fmt.Errorf("error occurred while running command %v on pod %s/%s: %s", commands, namespacedName.Namespace, |
| 23 | + namespacedName.Name, result.Stderr) |
| 24 | + } |
| 25 | + output := []string{} // Initialize with empty slice to ensure valid JSON when there's no output |
| 26 | + for _, line := range strings.Split(result.Stdout, "\n") { |
| 27 | + line = strings.TrimSpace(line) |
| 28 | + if line != "" { |
| 29 | + output = append(output, line) |
| 30 | + } |
| 31 | + } |
| 32 | + return output, nil |
| 33 | +} |
| 34 | + |
| 35 | +// filterLines filters lines using a regex pattern. |
| 36 | +func filterLines(lines []string, pattern string) ([]string, error) { |
| 37 | + if pattern == "" { |
| 38 | + return lines, nil |
| 39 | + } |
| 40 | + |
| 41 | + filterPattern, err := regexp.Compile(pattern) |
| 42 | + if err != nil { |
| 43 | + return nil, fmt.Errorf("invalid filter pattern %s: %w", pattern, err) |
| 44 | + } |
| 45 | + |
| 46 | + filtered := []string{} // Initialize with empty slice to ensure valid JSON when there's no output |
| 47 | + for _, line := range lines { |
| 48 | + if filterPattern.MatchString(line) { |
| 49 | + filtered = append(filtered, line) |
| 50 | + } |
| 51 | + } |
| 52 | + return filtered, nil |
| 53 | +} |
| 54 | + |
| 55 | +// limitLines limits the number of lines returned. |
| 56 | +func limitLines(lines []string, maxLines int) []string { |
| 57 | + if maxLines <= 0 { |
| 58 | + maxLines = defaultMaxLines |
| 59 | + } |
| 60 | + if len(lines) > maxLines { |
| 61 | + return lines[:maxLines] |
| 62 | + } |
| 63 | + return lines |
| 64 | +} |
| 65 | + |
| 66 | +// validateBridgeName validates that a bridge name is safe and non-empty. |
| 67 | +// Bridge names should only contain alphanumeric characters, hyphens, and underscores. |
| 68 | +func validateBridgeName(bridge string) error { |
| 69 | + if bridge == "" { |
| 70 | + return fmt.Errorf("bridge name cannot be empty") |
| 71 | + } |
| 72 | + |
| 73 | + // OVS bridge names typically follow naming conventions: alphanumeric, hyphens, underscores |
| 74 | + validBridgeName := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) |
| 75 | + if !validBridgeName.MatchString(bridge) { |
| 76 | + return fmt.Errorf("invalid bridge name %q: must contain only alphanumeric characters, hyphens, and underscores", bridge) |
| 77 | + } |
| 78 | + |
| 79 | + return nil |
| 80 | +} |
| 81 | + |
| 82 | +// validateFlowSpec validates that a flow specification is safe and non-empty. |
| 83 | +func validateFlowSpec(flow string) error { |
| 84 | + if flow == "" { |
| 85 | + return fmt.Errorf("flow specification cannot be empty") |
| 86 | + } |
| 87 | + |
| 88 | + // Check for potentially dangerous characters that shouldn't appear in flow specs |
| 89 | + // Flow specs should contain: alphanumeric, commas, equals, colons, periods, slashes, parentheses, brackets |
| 90 | + // We explicitly block: semicolons, pipes, backticks, dollar signs, and other shell metacharacters |
| 91 | + dangerousChars := regexp.MustCompile(`[;&|$` + "`" + `<>\\]`) |
| 92 | + if dangerousChars.MatchString(flow) { |
| 93 | + return fmt.Errorf("invalid flow specification: contains potentially dangerous characters") |
| 94 | + } |
| 95 | + |
| 96 | + return nil |
| 97 | +} |
| 98 | + |
| 99 | +// validateConntrackParams validates that conntrack additional parameters are safe. |
| 100 | +// Valid parameters for dpctl/dump-conntrack include: zone=N, mark=0xN, labels=0xN, -m, -s, etc. |
| 101 | +func validateConntrackParams(params []string) error { |
| 102 | + for _, param := range params { |
| 103 | + if param == "" { |
| 104 | + return fmt.Errorf("conntrack parameter cannot be empty") |
| 105 | + } |
| 106 | + |
| 107 | + // Check for potentially dangerous characters |
| 108 | + // Valid conntrack params should contain: alphanumeric, equals, hyphens, underscores, periods, colons, commas, forward slashes |
| 109 | + // We explicitly block: semicolons, pipes, backticks, dollar signs, ampersands, and other shell metacharacters |
| 110 | + dangerousChars := regexp.MustCompile(`[;&|$` + "`" + `<>\\()]`) |
| 111 | + if dangerousChars.MatchString(param) { |
| 112 | + return fmt.Errorf("invalid conntrack parameter %q: contains potentially dangerous characters", param) |
| 113 | + } |
| 114 | + |
| 115 | + // Additional validation for common parameter patterns |
| 116 | + // Valid patterns include: |
| 117 | + // - Single-char flags: -m, -s (single hyphen followed by single letter) |
| 118 | + // - Key=value pairs: zone=5, mark=0x1, src=10.0.0.1 (key must contain only alphanumeric, underscore, hyphen) |
| 119 | + validParam := regexp.MustCompile(`^(-[a-zA-Z]|[a-zA-Z0-9_-]+=[a-zA-Z0-9x.:,/_-]+)$`) |
| 120 | + if !validParam.MatchString(param) { |
| 121 | + return fmt.Errorf("invalid conntrack parameter format %q: must be a flag (e.g., '-m') or key=value pair (e.g., 'zone=5')", param) |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + return nil |
| 126 | +} |
0 commit comments