File tree Expand file tree Collapse file tree 3 files changed +31
-10
lines changed
Expand file tree Collapse file tree 3 files changed +31
-10
lines changed Original file line number Diff line number Diff line change @@ -113,17 +113,19 @@ const logger = createLogger('McpSettings')
113113 * can't be determined until resolution — but env vars only in the path/query
114114 * do NOT bypass the check.
115115 */
116- const ENV_VAR_PATTERN = / \{ \{ [ ^ } ] + \} \} / g
116+ const ENV_VAR_PATTERN = / \{ \{ [ ^ } ] + \} \} /
117117
118118function hasEnvVarInHostname ( url : string ) : boolean {
119119 // If the entire URL is an env var, hostname is unknown
120- if ( url . trim ( ) . replace ( ENV_VAR_PATTERN , '' ) . trim ( ) === '' ) return true
120+ const globalPattern = new RegExp ( ENV_VAR_PATTERN . source , 'g' )
121+ if ( url . trim ( ) . replace ( globalPattern , '' ) . trim ( ) === '' ) return true
121122 const protocolEnd = url . indexOf ( '://' )
122123 if ( protocolEnd === - 1 ) return ENV_VAR_PATTERN . test ( url )
124+ // Extract authority per RFC 3986 (terminated by /, ?, or #)
123125 const afterProtocol = url . substring ( protocolEnd + 3 )
124- const authorityEnd = afterProtocol . indexOf ( '/' )
126+ const authorityEnd = afterProtocol . search ( / [ / ? # ] / )
125127 const authority = authorityEnd === - 1 ? afterProtocol : afterProtocol . substring ( 0 , authorityEnd )
126- return new RegExp ( ENV_VAR_PATTERN . source ) . test ( authority )
128+ return ENV_VAR_PATTERN . test ( authority )
127129}
128130
129131function isDomainAllowed ( url : string | undefined , allowedDomains : string [ ] | null ) : boolean {
Original file line number Diff line number Diff line change @@ -178,6 +178,14 @@ describe('isMcpDomainAllowed', () => {
178178 false
179179 )
180180 } )
181+
182+ it ( 'rejects disallowed domain with env var in query but no path' , ( ) => {
183+ expect ( isMcpDomainAllowed ( 'https://evil.com?token={{SECRET}}' ) ) . toBe ( false )
184+ } )
185+
186+ it ( 'rejects disallowed domain with env var in fragment but no path' , ( ) => {
187+ expect ( isMcpDomainAllowed ( 'https://evil.com#{{SECTION}}' ) ) . toBe ( false )
188+ } )
181189 } )
182190
183191 describe ( 'env var security edge cases' , ( ) => {
@@ -276,6 +284,18 @@ describe('validateMcpDomain', () => {
276284 it ( 'does not throw for allowed URL with env var in path' , ( ) => {
277285 expect ( ( ) => validateMcpDomain ( 'https://allowed.com/{{PATH}}' ) ) . not . toThrow ( )
278286 } )
287+
288+ it ( 'throws for disallowed URL with env var in query but no path' , ( ) => {
289+ expect ( ( ) => validateMcpDomain ( 'https://evil.com?token={{SECRET}}' ) ) . toThrow (
290+ McpDomainNotAllowedError
291+ )
292+ } )
293+
294+ it ( 'throws for disallowed URL with env var in fragment but no path' , ( ) => {
295+ expect ( ( ) => validateMcpDomain ( 'https://evil.com#{{SECTION}}' ) ) . toThrow (
296+ McpDomainNotAllowedError
297+ )
298+ } )
279299 } )
280300 } )
281301} )
Original file line number Diff line number Diff line change @@ -30,19 +30,18 @@ function checkMcpDomain(url: string): string | null {
3030 * env vars in the path/query do NOT bypass the domain check.
3131 */
3232function hasEnvVarInHostname ( url : string ) : boolean {
33- const envVarPattern = createEnvVarPattern ( )
3433 // If the entire URL is an env var reference, hostname is unknown
35- if ( url . trim ( ) . replace ( envVarPattern , '' ) . trim ( ) === '' ) return true
34+ if ( url . trim ( ) . replace ( createEnvVarPattern ( ) , '' ) . trim ( ) === '' ) return true
3635 try {
37- // Extract the authority portion (between :// and the next / )
36+ // Extract the authority portion (between :// and the first /, ?, or # per RFC 3986 )
3837 const protocolEnd = url . indexOf ( '://' )
39- if ( protocolEnd === - 1 ) return envVarPattern . test ( url )
38+ if ( protocolEnd === - 1 ) return createEnvVarPattern ( ) . test ( url )
4039 const afterProtocol = url . substring ( protocolEnd + 3 )
41- const authorityEnd = afterProtocol . indexOf ( '/' )
40+ const authorityEnd = afterProtocol . search ( / [ / ? # ] / )
4241 const authority = authorityEnd === - 1 ? afterProtocol : afterProtocol . substring ( 0 , authorityEnd )
4342 return createEnvVarPattern ( ) . test ( authority )
4443 } catch {
45- return envVarPattern . test ( url )
44+ return createEnvVarPattern ( ) . test ( url )
4645 }
4746}
4847
You can’t perform that action at this time.
0 commit comments