@@ -2,6 +2,7 @@ const express = require('express');
22const cors = require ( 'cors' ) ;
33const fetch = require ( 'node-fetch' ) ;
44const { v4 : uuidv4 } = require ( 'uuid' ) ;
5+ const { URL } = require ( 'url' ) ;
56require ( 'dotenv' ) . config ( ) ;
67
78const { ClientBuilder } = require ( '@commercetools/ts-client' ) ;
@@ -12,9 +13,58 @@ const PORT = process.env.PORT || 3001;
1213
1314// Middleware
1415app . use ( cors ( ) ) ;
15- app . use ( express . json ( ) ) ;
16+ app . use ( express . json ( { limit : '10mb' } ) ) ; // Limit payload size for DoS protection
1617app . use ( express . static ( 'client/build' ) ) ;
1718
19+ // Security helpers
20+ function validateCommercetoolsUrl ( url ) {
21+ try {
22+ const parsedUrl = new URL ( url ) ;
23+ // Only allow commercetools domains to prevent SSRF
24+ const allowedHosts = [
25+ 'session.europe-west1.gcp.commercetools.com' ,
26+ 'session.us-central1.gcp.commercetools.com' ,
27+ 'session.australia-southeast1.gcp.commercetools.com' ,
28+ 'auth.europe-west1.gcp.commercetools.com' ,
29+ 'auth.us-central1.gcp.commercetools.com' ,
30+ 'auth.australia-southeast1.gcp.commercetools.com' ,
31+ 'api.europe-west1.gcp.commercetools.com' ,
32+ 'api.us-central1.gcp.commercetools.com' ,
33+ 'api.australia-southeast1.gcp.commercetools.com'
34+ ] ;
35+
36+ return allowedHosts . includes ( parsedUrl . hostname ) &&
37+ ( parsedUrl . protocol === 'https:' ) ;
38+ } catch ( error ) {
39+ return false ;
40+ }
41+ }
42+
43+ function validateInput ( input , type ) {
44+ if ( ! input ) return false ;
45+
46+ switch ( type ) {
47+ case 'uuid' :
48+ return / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i. test ( input ) ;
49+ case 'cartId' :
50+ // Allow UUID or commercetools ID format
51+ return / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i. test ( input ) ||
52+ / ^ [ a - z A - Z 0 - 9 - _ ] { 1 , 256 } $ / . test ( input ) ;
53+ case 'currency' :
54+ return / ^ [ A - Z ] { 3 } $ / . test ( input ) ;
55+ case 'country' :
56+ return / ^ [ A - Z ] { 2 } $ / . test ( input ) ;
57+ case 'locale' :
58+ return / ^ [ a - z ] { 2 } ( - [ A - Z ] { 2 } ) ? $ / . test ( input ) ;
59+ case 'string' :
60+ return typeof input === 'string' && input . length <= 1000 ;
61+ case 'number' :
62+ return typeof input === 'number' && input >= 0 && input <= 10000 ;
63+ default :
64+ return false ;
65+ }
66+ }
67+
1868// commercetools client setup
1969const projectKey = process . env . CTP_PROJECT_KEY ;
2070const scopes = process . env . CTP_SCOPES . split ( ' ' ) ;
@@ -49,8 +99,17 @@ app.post('/api/checkout/session', async (req, res) => {
4999 try {
50100 const { cartId, returnUrl, locale = 'en' } = req . body ;
51101
52- if ( ! cartId ) {
53- return res . status ( 400 ) . json ( { error : 'Cart ID is required' } ) ;
102+ // Input validation for DoS protection
103+ if ( ! cartId || ! validateInput ( cartId , 'cartId' ) ) {
104+ return res . status ( 400 ) . json ( { error : 'Valid Cart ID is required' } ) ;
105+ }
106+
107+ if ( locale && ! validateInput ( locale , 'locale' ) ) {
108+ return res . status ( 400 ) . json ( { error : 'Invalid locale format' } ) ;
109+ }
110+
111+ if ( returnUrl && ! validateInput ( returnUrl , 'string' ) ) {
112+ return res . status ( 400 ) . json ( { error : 'Invalid return URL format' } ) ;
54113 }
55114
56115 console . log ( `Creating checkout session for cart: ${ cartId } ` ) ;
@@ -83,8 +142,13 @@ app.post('/api/checkout/session', async (req, res) => {
83142 console . log ( 'Creating checkout session with correct structure:' , checkoutSessionData ) ;
84143
85144 // Call commercetools Checkout API to create session
86- // Using the correct endpoint structure
145+ // Using the correct endpoint structure with SSRF protection
87146 const sessionApiUrl = process . env . SESSION_API_URL || 'https://session.europe-west1.gcp.commercetools.com' ;
147+
148+ if ( ! validateCommercetoolsUrl ( sessionApiUrl ) ) {
149+ return res . status ( 400 ) . json ( { error : 'Invalid session API URL' } ) ;
150+ }
151+
88152 const checkoutResponse = await fetch (
89153 `${ sessionApiUrl } /${ projectKey } /sessions` ,
90154 {
@@ -139,7 +203,24 @@ app.post('/api/checkout/session', async (req, res) => {
139203// Sample cart creation endpoint for demo
140204app . post ( '/api/cart/create' , async ( req , res ) => {
141205 try {
142- const { currency = 'USD' , country = 'US' } = req . body ;
206+ const { currency = 'USD' , country = 'US' , productId, quantity = 1 } = req . body ;
207+
208+ // Input validation
209+ if ( currency && ! validateInput ( currency , 'currency' ) ) {
210+ return res . status ( 400 ) . json ( { error : 'Invalid currency format' } ) ;
211+ }
212+
213+ if ( country && ! validateInput ( country , 'country' ) ) {
214+ return res . status ( 400 ) . json ( { error : 'Invalid country format' } ) ;
215+ }
216+
217+ if ( productId && ! validateInput ( productId , 'string' ) ) {
218+ return res . status ( 400 ) . json ( { error : 'Invalid product ID format' } ) ;
219+ }
220+
221+ if ( typeof quantity !== 'undefined' && ! validateInput ( quantity , 'number' ) ) {
222+ return res . status ( 400 ) . json ( { error : 'Invalid quantity' } ) ;
223+ }
143224
144225 console . log ( 'Creating sample cart...' ) ;
145226
@@ -152,9 +233,9 @@ app.post('/api/cart/create', async (req, res) => {
152233 country : country ,
153234 lineItems : [
154235 {
155- productId : req . body . productId || await getSampleProductId ( ) ,
236+ productId : productId ,
156237 variantId : 1 ,
157- quantity : req . body . quantity || 1 ,
238+ quantity : quantity ,
158239 }
159240 ]
160241 }
@@ -284,26 +365,6 @@ async function getAccessToken() {
284365 return tokenData . access_token ;
285366}
286367
287- async function getSampleProductId ( ) {
288- try {
289- const products = await apiRoot
290- . productProjections ( )
291- . search ( )
292- . get ( {
293- queryArgs : { limit : 1 }
294- } )
295- . execute ( ) ;
296-
297- if ( products . body . results . length > 0 ) {
298- return products . body . results [ 0 ] . id ;
299- }
300-
301- throw new Error ( 'No products found in project' ) ;
302- } catch ( error ) {
303- console . error ( 'Error getting sample product:' , error ) ;
304- throw error ;
305- }
306- }
307368
308369// Start server
309370app . listen ( PORT , ( ) => {
0 commit comments