Skip to content

Commit 66aaf93

Browse files
authored
[v1.x] fix: prevent command injection in example URL opening (#2085)
1 parent 23a6157 commit 66aaf93

File tree

1 file changed

+21
-20
lines changed

1 file changed

+21
-20
lines changed

examples/snippets/clients/url_elicitation_client.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424

2525
import asyncio
2626
import json
27-
import subprocess
28-
import sys
27+
import logging
2928
import webbrowser
3029
from typing import Any
3130
from urllib.parse import urlparse
@@ -36,6 +35,8 @@
3635
from mcp.shared.exceptions import McpError, UrlElicitationRequiredError
3736
from mcp.types import URL_ELICITATION_REQUIRED
3837

38+
logger = logging.getLogger(__name__)
39+
3940

4041
async def handle_elicitation(
4142
context: RequestContext[ClientSession, Any],
@@ -56,15 +57,19 @@ async def handle_elicitation(
5657
)
5758

5859

60+
ALLOWED_SCHEMES = {"http", "https"}
61+
62+
5963
async def handle_url_elicitation(
6064
params: types.ElicitRequestParams,
6165
) -> types.ElicitResult:
6266
"""Handle URL mode elicitation - show security warning and optionally open browser.
6367
6468
This function demonstrates the security-conscious approach to URL elicitation:
65-
1. Display the full URL and domain for user inspection
66-
2. Show the server's reason for requesting this interaction
67-
3. Require explicit user consent before opening any URL
69+
1. Validate the URL scheme before prompting the user
70+
2. Display the full URL and domain for user inspection
71+
3. Show the server's reason for requesting this interaction
72+
4. Require explicit user consent before opening any URL
6873
"""
6974
# Extract URL parameters - these are available on URL mode requests
7075
url = getattr(params, "url", None)
@@ -75,6 +80,12 @@ async def handle_url_elicitation(
7580
print("Error: No URL provided in elicitation request")
7681
return types.ElicitResult(action="cancel")
7782

83+
# Reject dangerous URL schemes before prompting the user
84+
parsed = urlparse(str(url))
85+
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
86+
print(f"\nRejecting URL with disallowed scheme '{parsed.scheme}': {url}")
87+
return types.ElicitResult(action="decline")
88+
7889
# Extract domain for security display
7990
domain = extract_domain(url)
8091

@@ -105,7 +116,11 @@ async def handle_url_elicitation(
105116

106117
# Open the browser
107118
print(f"\nOpening browser to: {url}")
108-
open_browser(url)
119+
try:
120+
webbrowser.open(url)
121+
except Exception:
122+
logger.exception("Failed to open browser")
123+
print(f"Please manually open: {url}")
109124

110125
print("Waiting for you to complete the interaction in your browser...")
111126
print("(The server will continue once you've finished)")
@@ -121,20 +136,6 @@ def extract_domain(url: str) -> str:
121136
return "unknown"
122137

123138

124-
def open_browser(url: str) -> None:
125-
"""Open URL in the default browser."""
126-
try:
127-
if sys.platform == "darwin":
128-
subprocess.run(["open", url], check=False)
129-
elif sys.platform == "win32":
130-
subprocess.run(["start", url], shell=True, check=False)
131-
else:
132-
webbrowser.open(url)
133-
except Exception as e:
134-
print(f"Failed to open browser: {e}")
135-
print(f"Please manually open: {url}")
136-
137-
138139
async def call_tool_with_error_handling(
139140
session: ClientSession,
140141
tool_name: str,

0 commit comments

Comments
 (0)