11import  {  platform ,  release  }  from  'os' 
2- const  HOST_REGEX  =  / ^ (? ! \w + : \/ \/ ) ( [ \w - : ] + \. ) + ( [ \w - : ] + ) (?: : ( \d + ) ) ? (? ! : ) $ / 
2+ const  HOST_REGEX  =  / ^ (? ! (?: (?: h t t p s ? | f t p ) : \/ \/ | i n t e r n a l | l o c a l h o s t | (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \. ) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) ) ) (?: [ \w - ] + \. c o n t e n t s t a c k \. (?: i o | c o m ) (?: : [ ^ \/ \s : ] + ) ? | [ \w - ] + (?: \. [ \w - ] + ) * (?: : [ ^ \/ \s : ] + ) ? ) (? ! [ \/ ? # ] ) $ /    // eslint-disable-line 
33
44export  function  isHost  ( host )  { 
5+   if  ( ! host )  return  false 
56  return  HOST_REGEX . test ( host ) 
67} 
78
@@ -122,6 +123,21 @@ const isValidURL = (url) => {
122123      return  false 
123124    } 
124125
126+     const  officialDomains  =  [ 
127+       'api.contentstack.io' , 
128+       'eu-api.contentstack.com' , 
129+       'azure-na-api.contentstack.com' , 
130+       'azure-eu-api.contentstack.com' , 
131+       'gcp-na-api.contentstack.com' , 
132+       'gcp-eu-api.contentstack.com' 
133+     ] 
134+     const  isContentstackDomain  =  officialDomains . some ( domain  => 
135+       parsedURL . hostname  ===  domain  ||  parsedURL . hostname . endsWith ( '.'  +  domain ) 
136+     ) 
137+     if  ( isContentstackDomain  &&  parsedURL . protocol  !==  'https:' )  { 
138+       return  false 
139+     } 
140+ 
125141    // Prevent IP addresses in URLs to avoid internal network access 
126142    const  ipv4Regex  =  / ^ ( \d { 1 , 3 } \. ) { 3 } \d { 1 , 3 } $ / 
127143    const  ipv6Regex  =  / ^ \[ ? ( [ 0 - 9 a - f A - F ] { 0 , 4 } : ) { 2 , 7 } [ 0 - 9 a - f A - F ] { 0 , 4 } \] ? $ / 
@@ -137,15 +153,16 @@ const isValidURL = (url) => {
137153    } 
138154
139155    return  isAllowedHost ( parsedURL . hostname ) 
140-   }  catch  ( error )   { 
156+   }  catch  { 
141157    // If URL parsing fails, it might be a relative URL without protocol 
142158    // Allow it if it doesn't contain protocol indicators or suspicious patterns 
143-     return  ! url . includes ( '://' )  &&  ! url . includes ( '\\' )  &&  ! url . includes ( '@' ) 
159+     return  ! url ? .includes ( '://' )  &&  ! url ? .includes ( '\\' )  &&  ! url ? .includes ( '@' ) 
144160  } 
145161} 
146162
147163const  isAllowedHost  =  ( hostname )  =>  { 
148164  // Define allowed domains for Contentstack API 
165+   // Official Contentstack domains 
149166  const  allowedDomains  =  [ 
150167    'api.contentstack.io' , 
151168    'eu-api.contentstack.com' , 
@@ -172,20 +189,49 @@ const isAllowedHost = (hostname) => {
172189  } 
173190
174191  // Check if hostname is in allowed domains or is a subdomain of allowed domains 
175-   return  allowedDomains . some ( domain  =>  { 
192+   const   isContentstackDomain   =  allowedDomains . some ( domain  =>  { 
176193    return  hostname  ===  domain  ||  hostname . endsWith ( '.'  +  domain ) 
177194  } ) 
195+ 
196+   // If it's not a Contentstack domain, validate custom hostname 
197+   if  ( ! isContentstackDomain )  { 
198+     // Prevent internal/reserved IP ranges and localhost variants 
199+     const  ipv4Regex  =  / ^ ( \d { 1 , 3 } \. ) { 3 } \d { 1 , 3 } $ / 
200+     if  ( hostname ?. match ( ipv4Regex ) )  { 
201+       const  parts  =  hostname . split ( '.' ) 
202+       const  firstOctet  =  parseInt ( parts [ 0 ] ) 
203+       // Only block private IP ranges 
204+       if  ( firstOctet  ===  10  ||  firstOctet  ===  192  ||  firstOctet  ===  127 )  { 
205+         return  false 
206+       } 
207+     } 
208+     // Allow custom domains that don't match dangerous patterns 
209+     return  ! hostname . includes ( 'file://' )  && 
210+            ! hostname . includes ( '\\' )  && 
211+            ! hostname . includes ( '@' )  && 
212+            hostname  !==  'localhost' 
213+   } 
214+ 
215+   return  isContentstackDomain 
178216} 
179217
180218export  const  validateAndSanitizeConfig  =  ( config )  =>  { 
181-   if  ( ! config  ||  ! config . url )  { 
182-     throw  new  Error ( 'Invalid request configuration: missing URL' ) 
219+   if  ( ! config ?. url  ||  typeof   config ? .url   !==   'string' )  { 
220+     throw  new  Error ( 'Invalid request configuration: missing or invalid  URL' ) 
183221  } 
184222
185223  // Validate the URL to prevent SSRF attacks 
186224  if  ( ! isValidURL ( config . url ) )  { 
187225    throw  new  Error ( `SSRF Prevention: URL "${ config . url }  ) 
188226  } 
189227
190-   return  config 
228+   // Additional validation for baseURL if present 
229+   if  ( config . baseURL  &&  typeof  config . baseURL  ===  'string'  &&  ! isValidURL ( config . baseURL ) )  { 
230+     throw  new  Error ( `SSRF Prevention: Base URL "${ config . baseURL }  ) 
231+   } 
232+ 
233+   return  { 
234+     ...config , 
235+     url : config . url . trim ( )  // Sanitize URL by removing whitespace 
236+   } 
191237} 
0 commit comments