Skip to content

Commit 9b4bce0

Browse files
committed
chore: Validate and sanitize URL before making request for SSRF prevention
1 parent fd1a567 commit 9b4bce0

File tree

2 files changed

+70
-5
lines changed

2 files changed

+70
-5
lines changed

lib/core/Util.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,63 @@ 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+
// Allow relative URLs (they are safe as they use the same origin)
108+
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
109+
return true
110+
}
111+
112+
// Only validate absolute URLs for SSRF protection
113+
const parsedURL = new URL(url)
114+
return isAllowedHost(parsedURL.hostname)
115+
} catch (error) {
116+
// If URL parsing fails, it might be a relative URL without protocol
117+
// Allow it if it doesn't contain protocol indicators
118+
return !url.includes('://') && !url.includes('\\')
119+
}
120+
}
121+
122+
const isAllowedHost = (hostname) => {
123+
// Define allowed domains for Contentstack API
124+
const allowedDomains = [
125+
'api.contentstack.io',
126+
'eu-api.contentstack.com',
127+
'azure-na-api.contentstack.com',
128+
'azure-eu-api.contentstack.com',
129+
'gcp-na-api.contentstack.com',
130+
'gcp-eu-api.contentstack.com'
131+
]
132+
133+
// Check for localhost/development environments
134+
const localhostPatterns = [
135+
'localhost',
136+
'127.0.0.1',
137+
'0.0.0.0'
138+
]
139+
140+
// Allow localhost for development
141+
if (localhostPatterns.includes(hostname)) {
142+
return true
143+
}
144+
145+
// Check if hostname is in allowed domains or is a subdomain of allowed domains
146+
return allowedDomains.some(domain => {
147+
return hostname === domain || hostname.endsWith('.' + domain)
148+
})
149+
}
150+
151+
export const validateAndSanitizeConfig = (config) => {
152+
if (!config || !config.url) {
153+
throw new Error('Invalid request configuration: missing URL')
154+
}
155+
156+
// Validate the URL to prevent SSRF attacks
157+
if (!isValidURL(config.url)) {
158+
throw new Error(`SSRF Prevention: URL "${config.url}" is not allowed`)
159+
}
160+
161+
return config
162+
}

lib/core/concurrency-queue.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Axios from 'axios'
22
import OAuthHandler from './oauthHandler'
3+
import { validateAndSanitizeConfig } from './Util'
4+
35
const defaultConfig = {
46
maxRequests: 5,
57
retryLimit: 5,
@@ -129,7 +131,8 @@ export function ConcurrencyQueue ({ axios, config }) {
129131
}
130132

131133
// Retry the request
132-
axios(updateRequestConfig(error, `Network retry ${attempt}`, delay))
134+
const sanitizedConfig = validateAndSanitizeConfig(updateRequestConfig(error, `Network retry ${attempt}`, delay))
135+
axios(sanitizedConfig)
133136
.then(resolve)
134137
.catch((retryError) => {
135138
// Check if this is still a transient error and we can retry again
@@ -385,8 +388,9 @@ export function ConcurrencyQueue ({ axios, config }) {
385388
// Cool down the running requests
386389
delay(wait, response.status === 401)
387390
error.config.retryCount = networkError
388-
// deepcode ignore Ssrf: URL is dynamic
389-
return axios(updateRequestConfig(error, retryErrorType, wait))
391+
// SSRF Prevention: Validate URL before making request
392+
const sanitizedConfig = validateAndSanitizeConfig(updateRequestConfig(error, retryErrorType, wait))
393+
return axios(sanitizedConfig)
390394
}
391395
if (this.config.retryCondition && this.config.retryCondition(error)) {
392396
retryErrorType = error.response ? `Error with status: ${response.status}` : `Error Code:${error.code}`
@@ -416,8 +420,9 @@ export function ConcurrencyQueue ({ axios, config }) {
416420
error.config.retryCount = retryCount
417421
return new Promise(function (resolve) {
418422
return setTimeout(function () {
419-
// deepcode ignore Ssrf: URL is dynamic
420-
return resolve(axios(updateRequestConfig(error, retryErrorType, delaytime)))
423+
// SSRF Prevention: Validate URL before making request
424+
const sanitizedConfig = validateAndSanitizeConfig(updateRequestConfig(error, retryErrorType, delaytime))
425+
return resolve(axios(sanitizedConfig))
421426
}, delaytime)
422427
})
423428
}

0 commit comments

Comments
 (0)