Skip to content

Commit ee4cb1f

Browse files
authored
Merge pull request #276 from trungutt/validate-authorization-url
Secure OpenBrowser function against URL injection
2 parents 2d6638d + 3b756da commit ee4cb1f

1 file changed

Lines changed: 119 additions & 6 deletions

File tree

pkg/oauth/utils.go

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)