Skip to content

Commit b9431d4

Browse files
authored
fix: prevent command injection in example URL opening (#2082)
1 parent e82203b commit b9431d4

File tree

1 file changed

+18
-20
lines changed

1 file changed

+18
-20
lines changed

examples/snippets/clients/url_elicitation_client.py

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

2525
import asyncio
2626
import json
27-
import subprocess
28-
import sys
2927
import webbrowser
3028
from typing import Any
3129
from urllib.parse import urlparse
@@ -56,15 +54,19 @@ async def handle_elicitation(
5654
)
5755

5856

57+
ALLOWED_SCHEMES = {"http", "https"}
58+
59+
5960
async def handle_url_elicitation(
6061
params: types.ElicitRequestParams,
6162
) -> types.ElicitResult:
6263
"""Handle URL mode elicitation - show security warning and optionally open browser.
6364
6465
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
66+
1. Validate the URL scheme before prompting the user
67+
2. Display the full URL and domain for user inspection
68+
3. Show the server's reason for requesting this interaction
69+
4. Require explicit user consent before opening any URL
6870
"""
6971
# Extract URL parameters - these are available on URL mode requests
7072
url = getattr(params, "url", None)
@@ -75,6 +77,12 @@ async def handle_url_elicitation(
7577
print("Error: No URL provided in elicitation request")
7678
return types.ElicitResult(action="cancel")
7779

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

@@ -105,7 +113,11 @@ async def handle_url_elicitation(
105113

106114
# Open the browser
107115
print(f"\nOpening browser to: {url}")
108-
open_browser(url)
116+
try:
117+
webbrowser.open(url)
118+
except Exception as e:
119+
print(f"Failed to open browser: {e}")
120+
print(f"Please manually open: {url}")
109121

110122
print("Waiting for you to complete the interaction in your browser...")
111123
print("(The server will continue once you've finished)")
@@ -121,20 +133,6 @@ def extract_domain(url: str) -> str:
121133
return "unknown"
122134

123135

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-
138136
async def call_tool_with_error_handling(
139137
session: ClientSession,
140138
tool_name: str,

0 commit comments

Comments
 (0)