1
1
import { 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
3
3
4
4
export function isHost ( host ) {
5
+ if ( ! host ) return false
5
6
return HOST_REGEX . test ( host )
6
7
}
7
8
@@ -122,6 +123,21 @@ const isValidURL = (url) => {
122
123
return false
123
124
}
124
125
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
+
125
141
// Prevent IP addresses in URLs to avoid internal network access
126
142
const ipv4Regex = / ^ ( \d { 1 , 3 } \. ) { 3 } \d { 1 , 3 } $ /
127
143
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) => {
137
153
}
138
154
139
155
return isAllowedHost ( parsedURL . hostname )
140
- } catch ( error ) {
156
+ } catch {
141
157
// If URL parsing fails, it might be a relative URL without protocol
142
158
// 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 ( '@' )
144
160
}
145
161
}
146
162
147
163
const isAllowedHost = ( hostname ) => {
148
164
// Define allowed domains for Contentstack API
165
+ // Official Contentstack domains
149
166
const allowedDomains = [
150
167
'api.contentstack.io' ,
151
168
'eu-api.contentstack.com' ,
@@ -172,20 +189,49 @@ const isAllowedHost = (hostname) => {
172
189
}
173
190
174
191
// 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 => {
176
193
return hostname === domain || hostname . endsWith ( '.' + domain )
177
194
} )
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
178
216
}
179
217
180
218
export 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' )
183
221
}
184
222
185
223
// Validate the URL to prevent SSRF attacks
186
224
if ( ! isValidURL ( config . url ) ) {
187
225
throw new Error ( `SSRF Prevention: URL "${ config . url } " is not allowed` )
188
226
}
189
227
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
+ }
191
237
}
0 commit comments