Skip to content

Commit 2b8b377

Browse files
committed
Merge branch 'sanitise-urls'
2 parents de9e20b + 960631b commit 2b8b377

File tree

5 files changed

+30
-10
lines changed

5 files changed

+30
-10
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"deploy:site": "npx wrangler deploy"
3030
},
3131
"dependencies": {
32-
"@modelcontextprotocol/sdk": "^1.11.0"
32+
"@modelcontextprotocol/sdk": "^1.11.0",
33+
"strict-url-sanitise": "^0.0.1"
3334
},
3435
"devDependencies": {
3536
"@axodotdev/oranda": "^0.6.5",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth/browser-provider.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// browser-provider.ts
22
import { OAuthClientInformation, OAuthMetadata, OAuthTokens, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
33
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
4+
import { sanitizeUrl } from 'strict-url-sanitise'
45
// Assuming StoredState is defined in ./types.js and includes fields for provider options
56
import { StoredState } from './types.js' // Adjust path if necessary
67

@@ -29,15 +30,16 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
2930
this.serverUrlHash = this.hashString(serverUrl)
3031
this.clientName = options.clientName || 'MCP Browser Client'
3132
this.clientUri = options.clientUri || (typeof window !== 'undefined' ? window.location.origin : '')
32-
this.callbackUrl =
33+
this.callbackUrl = sanitizeUrl(
3334
options.callbackUrl ||
34-
(typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback')
35+
(typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback'),
36+
)
3537
}
3638

3739
// --- SDK Interface Methods ---
3840

3941
get redirectUrl(): string {
40-
return this.callbackUrl
42+
return sanitizeUrl(this.callbackUrl)
4143
}
4244

4345
get clientMetadata(): OAuthClientMetadata {
@@ -143,13 +145,16 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
143145
authorizationUrl.searchParams.set('state', state)
144146
const authUrlString = authorizationUrl.toString()
145147

148+
// Sanitize the authorization URL to prevent XSS attacks
149+
const sanitizedAuthUrl = sanitizeUrl(authUrlString)
150+
146151
// Persist the exact auth URL in case the popup fails and manual navigation is needed
147-
localStorage.setItem(this.getKey('last_auth_url'), authUrlString)
152+
localStorage.setItem(this.getKey('last_auth_url'), sanitizedAuthUrl)
148153

149154
// Attempt to open the popup
150155
const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' // Make configurable if needed
151156
try {
152-
const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures)
157+
const popup = window.open(sanitizedAuthUrl, `mcp_auth_${this.serverUrlHash}`, popupFeatures)
153158

154159
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
155160
console.warn(
@@ -175,7 +180,8 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
175180
* Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback.
176181
*/
177182
getLastAttemptedAuthUrl(): string | null {
178-
return localStorage.getItem(this.getKey('last_auth_url'))
183+
const storedUrl = localStorage.getItem(this.getKey('last_auth_url'))
184+
return storedUrl ? sanitizeUrl(storedUrl) : null
179185
}
180186

181187
clearStorage(): number {

src/react/useMcp.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprot
66
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' // Added
77
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
88
import { auth, UnauthorizedError, OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
9+
import { sanitizeUrl } from 'strict-url-sanitise'
910
import { BrowserOAuthClientProvider } from '../auth/browser-provider.js' // Adjust path
1011
import { assert } from '../utils/assert.js' // Adjust path
1112
import type { UseMcpOptions, UseMcpResult } from './types.js' // Adjust path
@@ -23,7 +24,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
2324
url,
2425
clientName,
2526
clientUri,
26-
callbackUrl = typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback',
27+
callbackUrl = typeof window !== 'undefined'
28+
? sanitizeUrl(new URL('/oauth/callback', window.location.origin).toString())
29+
: '/oauth/callback',
2730
storageKeyPrefix = 'mcp:auth',
2831
clientConfig = {},
2932
customHeaders = {},
@@ -189,7 +192,9 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
189192
authProvider: authProviderRef.current,
190193
requestInit: { headers: customHeaders },
191194
}
192-
const targetUrl = new URL(url)
195+
// Sanitize the URL to prevent XSS attacks from malicious server URLs
196+
const sanitizedUrl = sanitizeUrl(url)
197+
const targetUrl = new URL(sanitizedUrl)
193198

194199
if (transportType === 'http') {
195200
transportInstance = new StreamableHTTPClientTransport(targetUrl, commonOptions)

wrangler.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@
2020
"observability": {
2121
"enabled": true
2222
}
23-
}
23+
}

0 commit comments

Comments
 (0)