@@ -32,6 +32,7 @@ const PROBE_FILE = {
3232 mimeType : 'text/plain' ,
3333 data : 'data:text/plain;base64,cHJvYmU=' ,
3434} as const
35+ const DOT_SEGMENT_ERROR = 'Tool request URL cannot contain "." or ".." path segments'
3536const EXCEL_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
3637
3738function createSchemaProbeParams (
@@ -79,6 +80,12 @@ function isAbsoluteHttpUrl(url: string): boolean {
7980 }
8081}
8182
83+ function hasDotDotPathSegment ( url : string ) : boolean {
84+ const pathStart = url . indexOf ( '/' , url . indexOf ( '//' ) + 2 )
85+ if ( pathStart === - 1 ) return false
86+ return url . slice ( pathStart ) . split ( / [ ? # ] / ) [ 0 ] . split ( '/' ) . includes ( '..' )
87+ }
88+
8289function createRequestTool (
8390 url : string | ( ( params : Record < string , unknown > ) => string )
8491) : ToolConfig {
@@ -129,6 +136,40 @@ describe('external request transport', () => {
129136 ) . toBe ( 'https://example.com' )
130137 } )
131138
139+ it . each ( [
140+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/..' ,
141+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/../../../v0/inboxes/other' ,
142+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/.' ,
143+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/%2e%2E' ,
144+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/.%2e?force=true' ,
145+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/.\t.' ,
146+ 'https://api.example.com/v0/inboxes/inbox_1\\drafts\\..' ,
147+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/..\u0001' ,
148+ ' https://api.example.com/v0/inboxes/inbox_1/drafts/..\u0000 ' ,
149+ ] ) ( 'rejects a URL whose path resolves a dot segment: %s' , ( url ) => {
150+ expect ( ( ) =>
151+ prepareToolRequest (
152+ createRequestTool ( ( ) => url ) ,
153+ { }
154+ )
155+ ) . toThrow ( DOT_SEGMENT_ERROR )
156+ } )
157+
158+ it . each ( [
159+ 'https://my-app.vercel.app/v1/domains/example.com' ,
160+ 'https://api.example.com/v1/files/..foo/foo../.env' ,
161+ 'https://api.example.com/v1/search?path=../x#..' ,
162+ 'https://api.example.com/' ,
163+ 'https://api.example.com/v1/files/..\u00a0' ,
164+ ] ) ( 'allows dots that are not whole path segments: %s' , ( url ) => {
165+ expect (
166+ prepareToolRequest (
167+ createRequestTool ( ( ) => url ) ,
168+ { }
169+ ) . url
170+ ) . toBe ( url )
171+ } )
172+
132173 it . each ( [
133174 [ 'http_request' , requestTool , { url : '/api/auth/oauth/token' , method : 'GET' } ] ,
134175 [ 'webhook_request' , webhookRequestTool , { url : '/api/auth/oauth/token' , body : { } } ] ,
@@ -188,12 +229,16 @@ describe('dynamic external request registry invariant', () => {
188229 isAbsoluteHttpUrl ( url ) ,
189230 `${ toolId } resolved ${ url } outside the external HTTP transport`
190231 ) . toBe ( true )
191- expect ( ( ) =>
232+ const prepare = ( ) =>
192233 prepareToolRequest (
193234 createRequestTool ( ( ) => url ) ,
194235 { }
195236 )
196- ) . not . toThrow ( )
237+ if ( hasDotDotPathSegment ( url ) ) {
238+ expect ( prepare , `${ toolId } dispatched ${ url } ` ) . toThrow ( DOT_SEGMENT_ERROR )
239+ } else {
240+ expect ( prepare , `${ toolId } rejected ${ url } ` ) . not . toThrow ( )
241+ }
197242 }
198243
199244 if ( observations . length === 0 ) continue
0 commit comments