@@ -5,8 +5,11 @@ import (
55 "encoding/base64"
66 "encoding/json"
77 "fmt"
8+ "log/slog"
9+ "net/url"
810 "os/exec"
911 "runtime"
12+ "strings"
1013)
1114
1215// StateData represents the data encoded in the OAuth state parameter.
@@ -91,24 +94,134 @@ func DecodeSessionIDFromState(state string) (string, error) {
9194 return stateData .SessionID , nil
9295}
9396
94- // OpenBrowser opens the default browser to the specified URL
95- func OpenBrowser (url string ) error {
97+ // ValidateAndSanitizeURL validates and sanitizes a URL for safe browser opening.
98+ //
99+ // This function implements security measures to prevent command injection and malicious URL schemes
100+ // based on security best practices outlined in the Veria Labs article on MCP to shell vulnerabilities.
101+ //
102+ // Security validations:
103+ // - Only allows http:// and https:// schemes (blocks javascript:, data:, file:, etc.)
104+ // - Validates URL format using Go's net/url package
105+ // - Prevents command injection by ensuring URL can be safely passed to system commands
106+ //
107+ // Returns the original URL if valid, or an error if the URL fails security validation.
108+ func ValidateAndSanitizeURL (rawURL string ) (string , error ) {
109+ if rawURL == "" {
110+ return "" , fmt .Errorf ("URL cannot be empty" )
111+ }
112+
113+ // Parse the URL to validate its format
114+ parsedURL , err := url .Parse (rawURL )
115+ if err != nil {
116+ slog .Warn ("Invalid URL format blocked" , "url" , rawURL , "error" , err )
117+ return "" , fmt .Errorf ("invalid URL format: %w" , err )
118+ }
119+
120+ // Validate scheme - only allow http and https
121+ scheme := strings .ToLower (parsedURL .Scheme )
122+ allowedSchemes := map [string ]bool {
123+ "http" : true ,
124+ "https" : true ,
125+ }
126+
127+ if ! allowedSchemes [scheme ] {
128+ slog .Warn ("Dangerous URL scheme blocked" , "url" , rawURL , "scheme" , scheme )
129+ return "" , fmt .Errorf ("URL scheme '%s' is not allowed. Only http:// and https:// are permitted" , scheme )
130+ }
131+
132+ // Ensure we have a valid host
133+ if parsedURL .Host == "" {
134+ slog .Warn ("URL with empty host blocked" , "url" , rawURL )
135+ return "" , fmt .Errorf ("URL must have a valid host" )
136+ }
137+
138+ // Additional safety checks for potential command injection
139+ // We need to be careful here - some characters like & are legitimate in URLs (query parameters)
140+ // but dangerous in shell contexts. We'll check for dangerous patterns rather than individual chars.
141+
142+ // Check for backticks (command substitution)
143+ if strings .Contains (rawURL , "`" ) {
144+ slog .Warn ("URL with backticks blocked" , "url" , rawURL )
145+ return "" , fmt .Errorf ("URL contains potentially dangerous character: `" )
146+ }
147+
148+ // Check for shell command substitution patterns
149+ if strings .Contains (rawURL , "$(" ) {
150+ slog .Warn ("URL with command substitution pattern blocked" , "url" , rawURL )
151+ return "" , fmt .Errorf ("URL contains potentially dangerous character: $" )
152+ }
153+
154+ // Check for command chaining outside of query parameters
155+ // Allow & in query strings but block it in host/path contexts where it might be dangerous
156+ urlParts := strings .SplitN (rawURL , "?" , 2 ) // Split on first ? to separate base URL from query
157+ baseURL := urlParts [0 ]
158+
159+ dangerousInBase := []string {";" , "|" , "<" , ">" }
160+ for _ , char := range dangerousInBase {
161+ if strings .Contains (baseURL , char ) {
162+ slog .Warn ("URL with dangerous character in base URL blocked" , "url" , rawURL , "char" , char )
163+ return "" , fmt .Errorf ("URL contains potentially dangerous character: %s" , char )
164+ }
165+ }
166+
167+ // Check for other dangerous patterns
168+ dangerousPatterns := []string {
169+ " && " , " || " , // Command chaining with spaces
170+ " ; " , // Command separation with spaces
171+ }
172+
173+ for _ , pattern := range dangerousPatterns {
174+ if strings .Contains (rawURL , pattern ) {
175+ slog .Warn ("URL with dangerous pattern blocked" , "url" , rawURL , "pattern" , pattern )
176+ return "" , fmt .Errorf ("URL contains potentially dangerous pattern: %s" , strings .TrimSpace (pattern ))
177+ }
178+ }
179+
180+ slog .Debug ("URL validated successfully" , "url" , rawURL )
181+ return rawURL , nil
182+ }
183+
184+ // OpenBrowser opens the default browser to the specified URL with security validation.
185+ //
186+ // This function now includes security measures to prevent command injection and malicious URL schemes.
187+ // All URLs are validated before being passed to system commands to ensure they are safe to open.
188+ //
189+ // Security features:
190+ // - URL validation and sanitization using ValidateAndSanitizeURL
191+ // - Logging of security events (blocked URLs, successful opens)
192+ // - Maintains backward compatibility for legitimate OAuth URLs
193+ func OpenBrowser (urlToOpen string ) error {
194+ // Validate and sanitize the URL before opening
195+ validatedURL , err := ValidateAndSanitizeURL (urlToOpen )
196+ if err != nil {
197+ slog .Error ("Blocked attempt to open unsafe URL" , "url" , urlToOpen , "error" , err )
198+ return fmt .Errorf ("URL validation failed: %w" , err )
199+ }
200+
96201 var cmd string
97202 var args []string
98203
99204 switch runtime .GOOS {
100205 case "windows" :
101206 cmd = "rundll32"
102- args = []string {"url.dll,FileProtocolHandler" , url }
207+ args = []string {"url.dll,FileProtocolHandler" , validatedURL }
103208 case "darwin" :
104209 cmd = "open"
105- args = []string {url }
210+ args = []string {validatedURL }
106211 case "linux" :
107212 cmd = "xdg-open"
108- args = []string {url }
213+ args = []string {validatedURL }
109214 default :
110215 return fmt .Errorf ("unsupported platform: %s" , runtime .GOOS )
111216 }
112217
113- return exec .Command (cmd , args ... ).Start ()
218+ slog .Info ("Opening browser with validated URL" , "url" , validatedURL , "platform" , runtime .GOOS )
219+
220+ err = exec .Command (cmd , args ... ).Start ()
221+ if err != nil {
222+ slog .Error ("Failed to execute browser command" , "cmd" , cmd , "args" , args , "error" , err )
223+ return fmt .Errorf ("failed to open browser: %w" , err )
224+ }
225+
226+ return nil
114227}
0 commit comments