1+ import { getErrorMessage } from '@sim/utils/errors'
12import { describe , expect , it , vi } from 'vitest'
23import { isInternalToolOperationRegistered } from '@/lib/internal/tool-operations/registry.server'
34import { requestTool } from '@/tools/http/request'
@@ -32,6 +33,7 @@ const PROBE_FILE = {
3233 mimeType : 'text/plain' ,
3334 data : 'data:text/plain;base64,cHJvYmU=' ,
3435} as const
36+ const DOT_SEGMENT_ERROR = 'Tool request URL cannot contain "." or ".." path segments'
3537const EXCEL_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
3638
3739function createSchemaProbeParams (
@@ -129,6 +131,37 @@ describe('external request transport', () => {
129131 ) . toBe ( 'https://example.com' )
130132 } )
131133
134+ it . each ( [
135+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/..' ,
136+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/../../../v0/inboxes/other' ,
137+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/.' ,
138+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/%2e%2E' ,
139+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/.%2e?force=true' ,
140+ 'https://api.example.com/v0/inboxes/inbox_1/drafts/.\t.' ,
141+ 'https://api.example.com/v0/inboxes/inbox_1\\drafts\\..' ,
142+ ] ) ( 'rejects a URL whose path resolves a dot segment: %s' , ( url ) => {
143+ expect ( ( ) =>
144+ prepareToolRequest (
145+ createRequestTool ( ( ) => url ) ,
146+ { }
147+ )
148+ ) . toThrow ( DOT_SEGMENT_ERROR )
149+ } )
150+
151+ it . each ( [
152+ 'https://my-app.vercel.app/v1/domains/example.com' ,
153+ 'https://api.example.com/v1/files/..foo/foo../.env' ,
154+ 'https://api.example.com/v1/search?path=../x#..' ,
155+ 'https://api.example.com/' ,
156+ ] ) ( 'allows dots that are not whole path segments: %s' , ( url ) => {
157+ expect (
158+ prepareToolRequest (
159+ createRequestTool ( ( ) => url ) ,
160+ { }
161+ ) . url
162+ ) . toBe ( url )
163+ } )
164+
132165 it . each ( [
133166 [ 'http_request' , requestTool , { url : '/api/auth/oauth/token' , method : 'GET' } ] ,
134167 [ 'webhook_request' , webhookRequestTool , { url : '/api/auth/oauth/token' , body : { } } ] ,
@@ -169,12 +202,12 @@ describe('dynamic external request registry invariant', () => {
169202 if ( typeof urlBuilder !== 'function' ) throw new Error ( `${ toolId } must have a dynamic URL` )
170203 const observations : string [ ] = [ ]
171204 const scenarios = [
172- createSchemaProbeParams ( tool , false ) ,
173- createSchemaProbeParams ( tool , true ) ,
174- createSchemaProbeParams ( tool , true , true ) ,
205+ { params : createSchemaProbeParams ( tool , false ) , adversarial : false } ,
206+ { params : createSchemaProbeParams ( tool , true ) , adversarial : false } ,
207+ { params : createSchemaProbeParams ( tool , true , true ) , adversarial : true } ,
175208 ]
176209
177- for ( const params of scenarios ) {
210+ for ( const { params, adversarial } of scenarios ) {
178211 let url : string
179212 try {
180213 url = urlBuilder ( params as never )
@@ -188,12 +221,20 @@ describe('dynamic external request registry invariant', () => {
188221 isAbsoluteHttpUrl ( url ) ,
189222 `${ toolId } resolved ${ url } outside the external HTTP transport`
190223 ) . toBe ( true )
191- expect ( ( ) =>
224+ const prepare = ( ) =>
192225 prepareToolRequest (
193226 createRequestTool ( ( ) => url ) ,
194227 { }
195228 )
196- ) . not . toThrow ( )
229+ if ( ! adversarial ) {
230+ expect ( prepare , `${ toolId } rejected ${ url } ` ) . not . toThrow ( )
231+ continue
232+ }
233+ try {
234+ prepare ( )
235+ } catch ( error ) {
236+ expect ( getErrorMessage ( error ) , `${ toolId } rejected ${ url } ` ) . toBe ( DOT_SEGMENT_ERROR )
237+ }
197238 }
198239
199240 if ( observations . length === 0 ) continue
0 commit comments