@@ -100,3 +100,135 @@ export default function getUserAgent (sdk, application, integration, feature) {
100
100
101
101
return `${ headerParts . filter ( ( item ) => item !== '' ) . join ( '; ' ) } ;`
102
102
}
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