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 } " is not allowed` )
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 } " is not allowed` )
231+ }
232+
233+ return {
234+ ...config ,
235+ url : config . url . trim ( ) // Sanitize URL by removing whitespace
236+ }
191237}
0 commit comments