@@ -100,3 +100,135 @@ export default function getUserAgent (sdk, application, integration, feature) {
100100
101101 return `${ headerParts . filter ( ( item ) => item !== '' ) . join ( '; ' ) } ;`
102102}
103+
104+ // URL validation functions to prevent SSRF attacks
105+ const isValidURL = ( url ) => {
106+ try {
107+ // Reject obviously malicious patterns early
108+ if ( url . includes ( '@' ) || url . includes ( 'file://' ) || url . includes ( 'ftp://' ) ) {
109+ return false
110+ }
111+
112+ // Allow relative URLs (they are safe as they use the same origin)
113+ if ( url . startsWith ( '/' ) || url . startsWith ( './' ) || url . startsWith ( '../' ) ) {
114+ return true
115+ }
116+
117+ // Only validate absolute URLs for SSRF protection
118+ const parsedURL = new URL ( url )
119+
120+ // Reject non-HTTP(S) protocols
121+ if ( ! [ 'http:' , 'https:' ] . includes ( parsedURL . protocol ) ) {
122+ return false
123+ }
124+
125+ // Prevent IP addresses in URLs to avoid internal network access
126+ const ipv4Regex = / ^ ( \d { 1 , 3 } \. ) { 3 } \d { 1 , 3 } $ /
127+ const ipv6Regex = / ^ \[ ? ( [ 0 - 9 a - f A - F ] { 0 , 4 } : ) { 2 , 7 } [ 0 - 9 a - f A - F ] { 0 , 4 } \] ? $ /
128+ if ( ipv4Regex . test ( parsedURL . hostname ) || ipv6Regex . test ( parsedURL . hostname ) ) {
129+ // Only allow localhost IPs in development
130+ const isDevelopment = process . env . NODE_ENV === 'development' ||
131+ process . env . NODE_ENV === 'test' ||
132+ ! process . env . NODE_ENV
133+ const localhostIPs = [ '127.0.0.1' , '0.0.0.0' , '::1' , 'localhost' ]
134+ if ( ! isDevelopment || ! localhostIPs . includes ( parsedURL . hostname ) ) {
135+ return false
136+ }
137+ }
138+
139+ return isAllowedHost ( parsedURL . hostname )
140+ } catch ( error ) {
141+ // If URL parsing fails, it might be a relative URL without protocol
142+ // Allow it if it doesn't contain protocol indicators or suspicious patterns
143+ if ( error instanceof TypeError ) {
144+ return ! url . includes ( '://' ) && ! url . includes ( '\\' ) && ! url . includes ( '@' )
145+ }
146+ return false
147+ }
148+ }
149+
150+ const isAllowedHost = ( hostname ) => {
151+ // Define allowed domains for Contentstack API
152+ const allowedDomains = [
153+ 'api.contentstack.io' ,
154+ 'eu-api.contentstack.com' ,
155+ 'au-api.contentstack.com' ,
156+ 'azure-na-api.contentstack.com' ,
157+ 'azure-eu-api.contentstack.com' ,
158+ 'gcp-na-api.contentstack.com' ,
159+ 'gcp-eu-api.contentstack.com'
160+ ]
161+
162+ // Check for localhost/development environments
163+ const localhostPatterns = [
164+ 'localhost' ,
165+ '127.0.0.1' ,
166+ '0.0.0.0'
167+ ]
168+
169+ // Only allow localhost in development environments to prevent SSRF in production
170+ const isDevelopment = process . env . NODE_ENV === 'development' ||
171+ process . env . NODE_ENV === 'test' ||
172+ ! process . env . NODE_ENV // Default to allowing in non-production if NODE_ENV is not set
173+
174+ if ( isDevelopment && localhostPatterns . includes ( hostname ) ) {
175+ return true
176+ }
177+
178+ // Check if hostname is in allowed domains or is a subdomain of allowed domains
179+ return allowedDomains . some ( domain => {
180+ return hostname === domain || hostname . endsWith ( '.' + domain )
181+ } )
182+ }
183+
184+ // Helper function to validate individual URL properties
185+ const validateURLProperty = ( config , prop ) => {
186+ if ( config [ prop ] && ! isValidURL ( config [ prop ] ) ) {
187+ throw new Error ( `SSRF Prevention: ${ prop } "${ config [ prop ] } " is not allowed` )
188+ }
189+ }
190+
191+ // Helper function to validate combined URL (baseURL + url)
192+ const validateCombinedURL = ( baseURL , url ) => {
193+ try {
194+ let fullURL
195+ // Handle relative URLs with baseURL
196+ if ( url . startsWith ( '/' ) || url . startsWith ( './' ) || url . startsWith ( '../' ) ) {
197+ fullURL = new URL ( url , baseURL ) . href
198+ } else {
199+ // If url is absolute, it overrides baseURL
200+ fullURL = url
201+ }
202+
203+ if ( ! isValidURL ( fullURL ) ) {
204+ throw new Error ( `SSRF Prevention: Combined URL "${ fullURL } " is not allowed` )
205+ }
206+ } catch ( error ) {
207+ if ( error . message . startsWith ( 'SSRF Prevention:' ) ) {
208+ throw error
209+ }
210+ throw new Error ( `SSRF Prevention: Invalid URL combination of baseURL "${ baseURL } " and url "${ url } "` )
211+ }
212+ }
213+
214+ export const validateAndSanitizeConfig = ( config ) => {
215+ if ( ! config ) {
216+ throw new Error ( 'Invalid request configuration: missing config' )
217+ }
218+
219+ // Validate all possible URL properties in axios config to prevent SSRF attacks
220+ const urlProperties = [ 'url' , 'baseURL' ]
221+ urlProperties . forEach ( prop => validateURLProperty ( config , prop ) )
222+
223+ // If we have both baseURL and url, validate the combined URL
224+ if ( config . baseURL && config . url ) {
225+ validateCombinedURL ( config . baseURL , config . url )
226+ }
227+
228+ // Ensure we have at least one URL property
229+ if ( ! config . url && ! config . baseURL ) {
230+ throw new Error ( 'Invalid request configuration: missing URL or baseURL' )
231+ }
232+
233+ return config
234+ }
0 commit comments