@@ -49,44 +49,147 @@ describe('isMcpDomainAllowed', () => {
4949 it ( 'allows empty string URL' , ( ) => {
5050 expect ( isMcpDomainAllowed ( '' ) ) . toBe ( true )
5151 } )
52+
53+ it ( 'allows env var URLs' , ( ) => {
54+ expect ( isMcpDomainAllowed ( '{{MCP_SERVER_URL}}' ) ) . toBe ( true )
55+ } )
56+
57+ it ( 'allows URLs with env vars anywhere' , ( ) => {
58+ expect ( isMcpDomainAllowed ( 'https://server.com/{{PATH}}' ) ) . toBe ( true )
59+ } )
5260 } )
5361
5462 describe ( 'when allowlist is configured' , ( ) => {
5563 beforeEach ( ( ) => {
5664 mockGetAllowedMcpDomainsFromEnv . mockReturnValue ( [ 'allowed.com' , 'internal.company.com' ] )
5765 } )
5866
59- it ( 'allows URLs on the allowlist' , ( ) => {
60- expect ( isMcpDomainAllowed ( 'https://allowed.com/mcp' ) ) . toBe ( true )
61- expect ( isMcpDomainAllowed ( 'https://internal.company.com/tools' ) ) . toBe ( true )
62- } )
67+ describe ( 'basic domain matching' , ( ) => {
68+ it ( 'allows URLs on the allowlist' , ( ) => {
69+ expect ( isMcpDomainAllowed ( 'https://allowed.com/mcp' ) ) . toBe ( true )
70+ expect ( isMcpDomainAllowed ( 'https://internal.company.com/tools' ) ) . toBe ( true )
71+ } )
6372
64- it ( 'rejects URLs not on the allowlist ' , ( ) => {
65- expect ( isMcpDomainAllowed ( 'https://evil .com/mcp' ) ) . toBe ( false )
66- } )
73+ it ( 'allows URLs with paths on allowlisted domains ' , ( ) => {
74+ expect ( isMcpDomainAllowed ( 'https://allowed .com/deep/path/to/ mcp' ) ) . toBe ( true )
75+ } )
6776
68- it ( 'rejects undefined URL (fail-closed)' , ( ) => {
69- expect ( isMcpDomainAllowed ( undefined ) ) . toBe ( false )
70- } )
77+ it ( 'allows URLs with query params on allowlisted domains' , ( ) => {
78+ expect ( isMcpDomainAllowed ( 'https://allowed.com/mcp?key=value&foo=bar' ) ) . toBe ( true )
79+ } )
80+
81+ it ( 'allows URLs with ports on allowlisted domains' , ( ) => {
82+ expect ( isMcpDomainAllowed ( 'https://allowed.com:8080/mcp' ) ) . toBe ( true )
83+ } )
7184
72- it ( 'rejects empty string URL (fail-closed)' , ( ) => {
73- expect ( isMcpDomainAllowed ( '' ) ) . toBe ( false )
85+ it ( 'allows HTTP URLs on allowlisted domains' , ( ) => {
86+ expect ( isMcpDomainAllowed ( 'http://allowed.com/mcp' ) ) . toBe ( true )
87+ } )
88+
89+ it ( 'matches case-insensitively' , ( ) => {
90+ expect ( isMcpDomainAllowed ( 'https://ALLOWED.COM/mcp' ) ) . toBe ( true )
91+ expect ( isMcpDomainAllowed ( 'https://Allowed.Com/mcp' ) ) . toBe ( true )
92+ } )
93+
94+ it ( 'rejects URLs not on the allowlist' , ( ) => {
95+ expect ( isMcpDomainAllowed ( 'https://evil.com/mcp' ) ) . toBe ( false )
96+ } )
97+
98+ it ( 'rejects subdomains of allowed domains' , ( ) => {
99+ expect ( isMcpDomainAllowed ( 'https://sub.allowed.com/mcp' ) ) . toBe ( false )
100+ } )
101+
102+ it ( 'rejects URLs with allowed domain in path only' , ( ) => {
103+ expect ( isMcpDomainAllowed ( 'https://evil.com/allowed.com/mcp' ) ) . toBe ( false )
104+ } )
74105 } )
75106
76- it ( 'rejects malformed URLs' , ( ) => {
77- expect ( isMcpDomainAllowed ( 'not-a-url' ) ) . toBe ( false )
107+ describe ( 'fail-closed behavior' , ( ) => {
108+ it ( 'rejects undefined URL' , ( ) => {
109+ expect ( isMcpDomainAllowed ( undefined ) ) . toBe ( false )
110+ } )
111+
112+ it ( 'rejects empty string URL' , ( ) => {
113+ expect ( isMcpDomainAllowed ( '' ) ) . toBe ( false )
114+ } )
115+
116+ it ( 'rejects malformed URLs' , ( ) => {
117+ expect ( isMcpDomainAllowed ( 'not-a-url' ) ) . toBe ( false )
118+ } )
119+
120+ it ( 'rejects URLs with no protocol' , ( ) => {
121+ expect ( isMcpDomainAllowed ( 'allowed.com/mcp' ) ) . toBe ( false )
122+ } )
78123 } )
79124
80- it ( 'matches case-insensitively' , ( ) => {
81- expect ( isMcpDomainAllowed ( 'https://ALLOWED.COM/mcp' ) ) . toBe ( true )
125+ describe ( 'env var handling — hostname bypass' , ( ) => {
126+ it ( 'allows entirely env var URL' , ( ) => {
127+ expect ( isMcpDomainAllowed ( '{{MCP_SERVER_URL}}' ) ) . toBe ( true )
128+ } )
129+
130+ it ( 'allows env var URL with whitespace' , ( ) => {
131+ expect ( isMcpDomainAllowed ( ' {{MCP_SERVER_URL}} ' ) ) . toBe ( true )
132+ } )
133+
134+ it ( 'allows multiple env vars composing the entire URL' , ( ) => {
135+ expect ( isMcpDomainAllowed ( '{{PROTOCOL}}{{HOST}}{{PATH}}' ) ) . toBe ( true )
136+ } )
137+
138+ it ( 'allows env var in hostname portion' , ( ) => {
139+ expect ( isMcpDomainAllowed ( 'https://{{MCP_HOST}}/mcp' ) ) . toBe ( true )
140+ } )
141+
142+ it ( 'allows env var as subdomain' , ( ) => {
143+ expect ( isMcpDomainAllowed ( 'https://{{TENANT}}.company.com/mcp' ) ) . toBe ( true )
144+ } )
145+
146+ it ( 'allows env var in port (authority)' , ( ) => {
147+ expect ( isMcpDomainAllowed ( 'https://{{HOST}}:{{PORT}}/mcp' ) ) . toBe ( true )
148+ } )
149+
150+ it ( 'allows env var as the full authority' , ( ) => {
151+ expect ( isMcpDomainAllowed ( 'https://{{MCP_HOST}}:{{MCP_PORT}}/api/mcp' ) ) . toBe ( true )
152+ } )
82153 } )
83154
84- it ( 'allows env var URLs without validating domain' , ( ) => {
85- expect ( isMcpDomainAllowed ( '{{MCP_SERVER_URL}}' ) ) . toBe ( true )
155+ describe ( 'env var handling — no bypass when only in path/query' , ( ) => {
156+ it ( 'rejects disallowed domain with env var in path' , ( ) => {
157+ expect ( isMcpDomainAllowed ( 'https://evil.com/{{MCP_PATH}}' ) ) . toBe ( false )
158+ } )
159+
160+ it ( 'rejects disallowed domain with env var in query' , ( ) => {
161+ expect ( isMcpDomainAllowed ( 'https://evil.com/mcp?key={{API_KEY}}' ) ) . toBe ( false )
162+ } )
163+
164+ it ( 'rejects disallowed domain with env var in fragment' , ( ) => {
165+ expect ( isMcpDomainAllowed ( 'https://evil.com/mcp#{{SECTION}}' ) ) . toBe ( false )
166+ } )
167+
168+ it ( 'allows allowlisted domain with env var in path' , ( ) => {
169+ expect ( isMcpDomainAllowed ( 'https://allowed.com/{{MCP_PATH}}' ) ) . toBe ( true )
170+ } )
171+
172+ it ( 'allows allowlisted domain with env var in query' , ( ) => {
173+ expect ( isMcpDomainAllowed ( 'https://allowed.com/mcp?key={{API_KEY}}' ) ) . toBe ( true )
174+ } )
175+
176+ it ( 'rejects disallowed domain with env var in both path and query' , ( ) => {
177+ expect ( isMcpDomainAllowed ( 'https://evil.com/{{PATH}}?token={{TOKEN}}&key={{KEY}}' ) ) . toBe (
178+ false
179+ )
180+ } )
86181 } )
87182
88- it ( 'allows URLs with embedded env vars' , ( ) => {
89- expect ( isMcpDomainAllowed ( 'https://{{MCP_HOST}}/mcp' ) ) . toBe ( true )
183+ describe ( 'env var security edge cases' , ( ) => {
184+ it ( 'rejects URL with env var only after allowed domain in path' , ( ) => {
185+ expect ( isMcpDomainAllowed ( 'https://evil.com/allowed.com/{{VAR}}' ) ) . toBe ( false )
186+ } )
187+
188+ it ( 'rejects URL trying to use env var to sneak past domain check via userinfo' , ( ) => {
189+ // https://evil.com@allowed .com would have hostname "allowed.com" per URL spec,
190+ // but https://{{VAR}}@evil .com has env var in authority so it bypasses
191+ expect ( isMcpDomainAllowed ( 'https://{{VAR}}@evil.com/mcp' ) ) . toBe ( true )
192+ } )
90193 } )
91194 } )
92195} )
@@ -108,39 +211,71 @@ describe('validateMcpDomain', () => {
108211 it ( 'does not throw for undefined URL' , ( ) => {
109212 expect ( ( ) => validateMcpDomain ( undefined ) ) . not . toThrow ( )
110213 } )
214+
215+ it ( 'does not throw for empty string' , ( ) => {
216+ expect ( ( ) => validateMcpDomain ( '' ) ) . not . toThrow ( )
217+ } )
111218 } )
112219
113220 describe ( 'when allowlist is configured' , ( ) => {
114221 beforeEach ( ( ) => {
115222 mockGetAllowedMcpDomainsFromEnv . mockReturnValue ( [ 'allowed.com' ] )
116223 } )
117224
118- it ( 'does not throw for allowed URLs' , ( ) => {
119- expect ( ( ) => validateMcpDomain ( 'https://allowed.com/mcp' ) ) . not . toThrow ( )
120- } )
225+ describe ( 'basic validation' , ( ) => {
226+ it ( 'does not throw for allowed URLs' , ( ) => {
227+ expect ( ( ) => validateMcpDomain ( 'https://allowed.com/mcp' ) ) . not . toThrow ( )
228+ } )
121229
122- it ( 'throws McpDomainNotAllowedError for disallowed URLs' , ( ) => {
123- expect ( ( ) => validateMcpDomain ( 'https://evil.com/mcp' ) ) . toThrow ( McpDomainNotAllowedError )
124- } )
230+ it ( 'throws McpDomainNotAllowedError for disallowed URLs' , ( ) => {
231+ expect ( ( ) => validateMcpDomain ( 'https://evil.com/mcp' ) ) . toThrow ( McpDomainNotAllowedError )
232+ } )
125233
126- it ( 'throws for undefined URL (fail-closed)' , ( ) => {
127- expect ( ( ) => validateMcpDomain ( undefined ) ) . toThrow ( McpDomainNotAllowedError )
128- } )
234+ it ( 'throws for undefined URL (fail-closed)' , ( ) => {
235+ expect ( ( ) => validateMcpDomain ( undefined ) ) . toThrow ( McpDomainNotAllowedError )
236+ } )
129237
130- it ( 'throws for malformed URLs' , ( ) => {
131- expect ( ( ) => validateMcpDomain ( 'not-a-url' ) ) . toThrow ( McpDomainNotAllowedError )
132- } )
238+ it ( 'throws for malformed URLs' , ( ) => {
239+ expect ( ( ) => validateMcpDomain ( 'not-a-url' ) ) . toThrow ( McpDomainNotAllowedError )
240+ } )
133241
134- it ( 'includes the rejected domain in the error message' , ( ) => {
135- expect ( ( ) => validateMcpDomain ( 'https://evil.com/mcp' ) ) . toThrow ( / e v i l \. c o m / )
136- } )
242+ it ( 'includes the rejected domain in the error message' , ( ) => {
243+ expect ( ( ) => validateMcpDomain ( 'https://evil.com/mcp' ) ) . toThrow ( / e v i l \. c o m / )
244+ } )
137245
138- it ( 'does not throw for env var URLs' , ( ) => {
139- expect ( ( ) => validateMcpDomain ( '{{MCP_SERVER_URL}}' ) ) . not . toThrow ( )
246+ it ( 'includes "(empty)" in error for undefined URL' , ( ) => {
247+ expect ( ( ) => validateMcpDomain ( undefined ) ) . toThrow ( / \( e m p t y \) / )
248+ } )
140249 } )
141250
142- it ( 'does not throw for URLs with embedded env vars' , ( ) => {
143- expect ( ( ) => validateMcpDomain ( 'https://{{MCP_HOST}}/mcp' ) ) . not . toThrow ( )
251+ describe ( 'env var handling' , ( ) => {
252+ it ( 'does not throw for entirely env var URL' , ( ) => {
253+ expect ( ( ) => validateMcpDomain ( '{{MCP_SERVER_URL}}' ) ) . not . toThrow ( )
254+ } )
255+
256+ it ( 'does not throw for env var in hostname' , ( ) => {
257+ expect ( ( ) => validateMcpDomain ( 'https://{{MCP_HOST}}/mcp' ) ) . not . toThrow ( )
258+ } )
259+
260+ it ( 'does not throw for env var in authority' , ( ) => {
261+ expect ( ( ) => validateMcpDomain ( 'https://{{HOST}}:{{PORT}}/mcp' ) ) . not . toThrow ( )
262+ } )
263+
264+ it ( 'throws for disallowed URL with env var only in path' , ( ) => {
265+ expect ( ( ) => validateMcpDomain ( 'https://evil.com/{{MCP_PATH}}' ) ) . toThrow (
266+ McpDomainNotAllowedError
267+ )
268+ } )
269+
270+ it ( 'throws for disallowed URL with env var only in query' , ( ) => {
271+ expect ( ( ) => validateMcpDomain ( 'https://evil.com/mcp?key={{API_KEY}}' ) ) . toThrow (
272+ McpDomainNotAllowedError
273+ )
274+ } )
275+
276+ it ( 'does not throw for allowed URL with env var in path' , ( ) => {
277+ expect ( ( ) => validateMcpDomain ( 'https://allowed.com/{{PATH}}' ) ) . not . toThrow ( )
278+ } )
144279 } )
145280 } )
146281} )
0 commit comments