Skip to content

Commit bf2b162

Browse files
Merge pull request #409 from contentstack/development
Staging
2 parents fbdadfd + 4336914 commit bf2b162

File tree

10 files changed

+924
-2556
lines changed

10 files changed

+924
-2556
lines changed

.talismanrc

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
fileignoreconfig:
2+
- filename: test/unit/globalField-test.js
3+
checksum: 25185e3400a12e10a043dc47502d8f30b7e1c4f2b6b4d3b8b55cdc19850c48bf
4+
- filename: lib/stack/index.js
5+
checksum: 6aab5edf85efb17951418b4dc4402889cd24c8d786c671185074aeb4d50f0242
6+
- filename: test/sanity-check/api/stack-test.js
7+
checksum: 198d5cf7ead33b079249dc3ecdee61a9c57453e93f1073ed0341400983e5aa53
8+
- filename: .github/workflows/secrets-scan.yml
9+
ignore_detectors:
10+
- filecontent
11+
- filename: package-lock.json
12+
checksum: b043facad4b4aca7a013730746bdb9cb9e9dfca1e5d6faf11c068fc2525569c0
213
- filename: .husky/pre-commit
314
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
415
- filename: test/sanity-check/api/user-test.js
516
checksum: 6bb8251aad584e09f4d963a913bd0007e5f6e089357a44c3fb1529e3fda5509d
6-
- filename: package-lock.json
7-
checksum: b9068b76378f5cedcae28adfff14b961289b3a0ddcd026fe3d026cfd877178a4
817
- filename: lib/stack/asset/index.js
918
checksum: b3358310e9cb2fb493d70890b7219db71e2202360be764465d505ef71907eefe
10-
version: ""
19+
- filename: test/sanity-check/api/previewToken-test.js
20+
checksum: 9a42e079b7c71f76932896a0d2390d86ac626678ab20d36821dcf962820a886c
21+
- filename: lib/stack/deliveryToken/index.js
22+
checksum: 51ae00f07f4cc75c1cd832b311c2e2482f04a8467a0139da6013ceb88fbdda2f
23+
- filename: lib/stack/deliveryToken/previewToken/index.js
24+
checksum: b506f33bffdd20dfc701f964370707f5d7b28a2c05c70665f0edb7b3c53c165b
25+
- filename: examples/robust-error-handling.js
26+
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
27+
version: "1.0"

examples/robust-error-handling.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Example: Configuring Robust Error Handling for Transient Network Failures
2+
// This example shows how to use the enhanced retry mechanisms in the Contentstack Management SDK
3+
4+
const contentstack = require('../lib/contentstack')
5+
6+
// Example 1: Basic configuration with enhanced network retry
7+
const clientWithBasicRetry = contentstack.client({
8+
api_key: 'your_api_key',
9+
management_token: 'your_management_token',
10+
// Enhanced network retry configuration
11+
retryOnNetworkFailure: true, // Enable network failure retries
12+
maxNetworkRetries: 3, // Max 3 attempts for network failures
13+
networkRetryDelay: 100, // Start with 100ms delay
14+
networkBackoffStrategy: 'exponential' // Use exponential backoff (100ms, 200ms, 400ms)
15+
})
16+
17+
// Example 2: Advanced configuration with fine-grained control
18+
const clientWithAdvancedRetry = contentstack.client({
19+
api_key: 'your_api_key',
20+
management_token: 'your_management_token',
21+
// Network failure retry settings
22+
retryOnNetworkFailure: true,
23+
retryOnDnsFailure: true, // Retry on DNS resolution failures (EAI_AGAIN)
24+
retryOnSocketFailure: true, // Retry on socket errors (ECONNRESET, ETIMEDOUT, etc.)
25+
retryOnHttpServerError: true, // Retry on HTTP 5xx errors
26+
maxNetworkRetries: 5, // Allow up to 5 network retries
27+
networkRetryDelay: 200, // Start with 200ms delay
28+
networkBackoffStrategy: 'exponential',
29+
30+
// Original retry settings (for non-network errors)
31+
retryOnError: true,
32+
retryLimit: 3,
33+
retryDelay: 500,
34+
35+
// Custom logging
36+
logHandler: (level, message) => {
37+
console.log(`[${level.toUpperCase()}] ${new Date().toISOString()}: ${message}`)
38+
}
39+
})
40+
41+
// Example 3: Conservative configuration for production
42+
const clientForProduction = contentstack.client({
43+
api_key: 'your_api_key',
44+
management_token: 'your_management_token',
45+
// Conservative retry settings for production
46+
retryOnNetworkFailure: true,
47+
maxNetworkRetries: 2, // Only 2 retries to avoid long delays
48+
networkRetryDelay: 300, // Longer initial delay
49+
networkBackoffStrategy: 'fixed', // Fixed delay instead of exponential
50+
51+
// Custom retry condition for additional control
52+
retryCondition: (error) => {
53+
// Custom logic: only retry on specific conditions
54+
return error.response && error.response.status >= 500
55+
}
56+
})
57+
58+
// Example usage with error handling
59+
async function demonstrateRobustErrorHandling () {
60+
try {
61+
const stack = clientWithAdvancedRetry.stack('your_stack_api_key')
62+
const contentTypes = await stack.contentType().query().find()
63+
console.log('Content types retrieved successfully:', contentTypes.items.length)
64+
} catch (error) {
65+
if (error.retryAttempts) {
66+
console.error(`Request failed after ${error.retryAttempts} retry attempts:`, error.message)
67+
console.error('Original error:', error.originalError?.code)
68+
} else {
69+
console.error('Request failed:', error.message)
70+
}
71+
}
72+
}
73+
74+
// The SDK will now automatically handle:
75+
// ✅ DNS resolution failures (EAI_AGAIN)
76+
// ✅ Socket errors (ECONNRESET, ETIMEDOUT, ECONNREFUSED)
77+
// ✅ HTTP timeouts (ECONNABORTED)
78+
// ✅ HTTP 5xx server errors (500-599)
79+
// ✅ Exponential backoff with configurable delays
80+
// ✅ Clear logging and user-friendly error messages
81+
82+
module.exports = {
83+
clientWithBasicRetry,
84+
clientWithAdvancedRetry,
85+
clientForProduction,
86+
demonstrateRobustErrorHandling
87+
}

lib/core/Util.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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-9a-fA-F]{0,4}:){2,7}[0-9a-fA-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

Comments
 (0)