@@ -361,7 +361,7 @@ export class McpClientOAuthProtocol {
361361 if ( typeof input !== "object" || input === null ) throw invalidOptionsError ( ) ;
362362 const authority = normalizeAuthority ( input . authority ) ;
363363 const client = normalizeClient ( input . client , authority ) ;
364- const redirectUri = requireSecureUrl ( input . redirectUri , { query : true } ) . href ;
364+ const redirectUri = requireRedirectUri ( input . redirectUri ) . href ;
365365 const scope = normalizeScopes ( input . scopes , authority ) ;
366366 throwIfAborted ( input . signal ) ;
367367 await this . #authorizeEndpoint( authority . authorizationEndpoint , {
@@ -778,7 +778,7 @@ function normalizeTransaction(
778778 if ( transaction . authorityDigest !== createAuthorityDigest ( authority ) ) {
779779 throw transactionInvalidError ( ) ;
780780 }
781- const redirectUri = requireSecureUrl ( transaction . redirectUri , { query : true } ) . href ;
781+ const redirectUri = requireRedirectUri ( transaction . redirectUri ) . href ;
782782 assertBoundedOpaqueValue ( transaction . clientId , MAX_CLIENT_ID_LENGTH ) ;
783783 if ( ! isClientAuthenticationMethod ( transaction . clientAuthenticationMethod ) ) {
784784 throw transactionInvalidError ( ) ;
@@ -1184,6 +1184,41 @@ function requireSecureUrl(value: string, options: { readonly query: boolean }):
11841184 return url ;
11851185}
11861186
1187+ function requireRedirectUri ( value : string ) : URL {
1188+ if ( typeof value !== "string" || value . length === 0 || value . length > MAX_URL_LENGTH ) {
1189+ throw authorityInvalidError ( ) ;
1190+ }
1191+ let url : URL ;
1192+ try {
1193+ url = new URL ( value ) ;
1194+ } catch {
1195+ throw authorityInvalidError ( ) ;
1196+ }
1197+ if (
1198+ url . username . length > 0 ||
1199+ url . password . length > 0 ||
1200+ url . hash . length > 0 ||
1201+ ( url . protocol !== "https:" && ! isLoopbackHttpRedirect ( url ) )
1202+ ) {
1203+ throw authorityInvalidError ( ) ;
1204+ }
1205+ return url ;
1206+ }
1207+
1208+ function isLoopbackHttpRedirect ( url : URL ) : boolean {
1209+ if ( url . protocol !== "http:" ) return false ;
1210+ const host = url . hostname . toLowerCase ( ) ;
1211+ if ( host === "localhost" || host === "[::1]" ) return true ;
1212+ const octets = host . split ( "." ) ;
1213+ return octets . length === 4 && octets [ 0 ] === "127" && octets . every ( isDecimalOctet ) ;
1214+ }
1215+
1216+ function isDecimalOctet ( value : string ) : boolean {
1217+ if ( ! / ^ \d { 1 , 3 } $ / u. test ( value ) ) return false ;
1218+ const parsed = Number ( value ) ;
1219+ return parsed >= 0 && parsed <= 255 && String ( parsed ) === value ;
1220+ }
1221+
11871222function requireCanonicalResource ( value : string ) : string {
11881223 const resource = requireSecureUrl ( value , { query : true } ) ;
11891224 if ( resource . href !== value ) throw authorityInvalidError ( ) ;
0 commit comments