From 8e88000ce96d6a827385ff4fbf3022295097c6bd Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Mon, 25 May 2026 19:35:48 +0500 Subject: [PATCH 1/5] Implement callsback security --- .../cartridge/controllers/MarketPay.js | 16 ++++ .../helpers/marketPaySignatureHelpers.js | 96 +++++++++++++++++++ .../meta/system-objecttype-extensions.xml | 9 ++ 3 files changed, 121 insertions(+) create mode 100644 cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js diff --git a/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js b/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js index 2b856ab..bb69139 100644 --- a/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js +++ b/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js @@ -9,8 +9,12 @@ var Logger = require('dw/system/Logger').getLogger('MarketPay', 'MarketPay'); var COHelpers = require('*/cartridge/scripts/helpers/marketPayCheckoutHelpers'); var ipHelpers = require('*/cartridge/scripts/helpers/ipHelpers'); var notificationHelpers = require('*/cartridge/scripts/helpers/marketPayNotificationHelpers'); +var signatureHelpers = require('*/cartridge/scripts/helpers/marketPaySignatureHelpers.js'); server.post('CallbackForm', server.middleware.https, function (req, res, next) { + if (!signatureHelpers.validateRequest(req)) { + throw new Error("Invalid request."); + } var languageCode = req.form.language; var formTemplateClass = req.form.form_template; @@ -34,6 +38,9 @@ server.post('PaymentSuccess', server.middleware.https, function (req, res, next) var order = null; try { + if (!signatureHelpers.validateRequest(req)) { + throw new Error("Invalid request."); + } orderID = req.form.shop_orderid; var orderXMLObject = new XML(req.form.xml); var transactions = orderXMLObject.Body.Transactions.Transaction; @@ -81,6 +88,9 @@ server.post('PaymentFail', server.middleware.https, function (req, res, next) { var order = null; try { + if (!signatureHelpers.validateRequest(req)) { + throw new Error("Invalid request."); + } var orderXMLObject = new XML(req.form.xml); var transactions = orderXMLObject.Body.Transactions.Transaction; var latestTxn = marketPayDataHelper.getLatestTransaction(transactions); @@ -115,6 +125,12 @@ server.post('PaymentFail', server.middleware.https, function (req, res, next) { * This controller is for asynchronous payments, when the acquirer returns an answer for payment request. */ server.post('PaymentNotification', server.middleware.https, function (req, res, next) { + if (!signatureHelpers.validateRequest(req)) { + res.setStatusCode(400); + res.json({ message: 'Invalid request.' }); + + return next(); + } // Ignore new status if (req.form.status === 'new') { res.setStatusCode(200); diff --git a/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js b/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js new file mode 100644 index 0000000..f44fbb5 --- /dev/null +++ b/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js @@ -0,0 +1,96 @@ +'use strict'; + +var Mac = require('dw/crypto/Mac'); +var Encoding = require('dw/crypto/Encoding'); +var Bytes = require('dw/util/Bytes'); +var Site = require('dw/system/Site'); + +// Matches Java URLEncoder: space→+, encode all chars except A-Za-z0-9 - _ . * +// encodeURIComponent leaves ( ) ! ~ ' unencoded but Java URLEncoder encodes them. +function formEncode(str) { + return encodeURIComponent(str) + .replace(/%20/g, '+') + .replace(/[!()'~]/g, function (c) { + return '%' + c.charCodeAt(0).toString(16).toUpperCase(); + }); +} + +function validateRequest(req) { + var secret = Site.getCurrent().getCustomPreferenceValue('marketPayCallbackSecret'); + + if (!secret) { + return true; // No secret configured, skip validation + } + + // Step 1: Parse the AltaPay-Signature header + var signatureHeader = req.httpHeaders.get('altapay-signature'); + + if (!signatureHeader) { + return false; + } + + var timestamp = null; + var signatures = []; + + signatureHeader.split(';').forEach(function (field) { + var trimmed = field.trim(); + if (trimmed.indexOf('t=') === 0) { + timestamp = trimmed.substring(2); + } else if (/^s\d+=/.test(trimmed)) { + signatures.push(trimmed.split('=')[1]); + } + }); + + if (!timestamp || signatures.length === 0) { + return false; + } + + // Step 2: Prepare the payload — rawBody + "." + timestamp + // SFCC parses application/x-www-form-urlencoded bodies into httpParameterMap on + // request arrival, so requestBodyAsString is always null for this content type. + // Reconstruct by re-encoding parameters in received order (Tomcat preserves it). + var paramMap = request.httpParameterMap; + var parts = []; + var paramNamesIter = paramMap.getParameterNames().iterator(); + while (paramNamesIter.hasNext()) { + var paramName = paramNamesIter.next(); + var stringValues = paramMap.get(paramName).getStringValues(); + if (stringValues.isEmpty()) { + // SFCC returns empty Collection for params submitted with no value (e.g. "error_message=") + parts.push(formEncode(paramName) + '='); + } else { + var valuesIter = stringValues.iterator(); + while (valuesIter.hasNext()) { + var paramValue = valuesIter.next(); + // SFCC strips trailing \n from parameter values; AltaPay appends \n after + if (paramName === 'xml') { + paramValue += '\n'; + } + parts.push(formEncode(paramName) + '=' + formEncode(paramValue)); + } + } + } + var rawBody = parts.join('&'); + + var mac = new Mac(Mac.HMAC_SHA_256); + var payload = rawBody + '.' + timestamp; + var calculatedHex = Encoding.toHex(mac.digest(new Bytes(payload, 'UTF-8'), new Bytes(secret, 'UTF-8'))); + var signatureValid = signatures.some(function (sig) { return calculatedHex === sig; }); + + if (!signatureValid) { + + var Logger = require('dw/system/Logger'); + var logger = Logger.getLogger('Marketpay', 'Marketpay'); + + logger.error(`MarketPay Signature doesn't match`); + logger.error('reconstructed rawBody: {0}', rawBody); + logger.error('calculated HMAC: {0}', calculatedHex); + return false; + } + + return true; +} + +module.exports = { + validateRequest: validateRequest +}; diff --git a/metadata/MarketPay-Headless/meta/system-objecttype-extensions.xml b/metadata/MarketPay-Headless/meta/system-objecttype-extensions.xml index d24b4fe..13e29e8 100644 --- a/metadata/MarketPay-Headless/meta/system-objecttype-extensions.xml +++ b/metadata/MarketPay-Headless/meta/system-objecttype-extensions.xml @@ -118,6 +118,14 @@ + + Callback Secret + + string + false + false + 0 + App URL This is an app URL where the customer will be @@ -189,6 +197,7 @@ + From 04c7d0c1cf676045f5a5729d1dd56cf0aa265b8b Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Mon, 25 May 2026 20:37:34 +0500 Subject: [PATCH 2/5] Remove logging for signature validation failure in validateRequest function --- .../cartridge/scripts/helpers/marketPaySignatureHelpers.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js b/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js index f44fbb5..9e08202 100644 --- a/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js +++ b/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPaySignatureHelpers.js @@ -78,13 +78,6 @@ function validateRequest(req) { var signatureValid = signatures.some(function (sig) { return calculatedHex === sig; }); if (!signatureValid) { - - var Logger = require('dw/system/Logger'); - var logger = Logger.getLogger('Marketpay', 'Marketpay'); - - logger.error(`MarketPay Signature doesn't match`); - logger.error('reconstructed rawBody: {0}', rawBody); - logger.error('calculated HMAC: {0}', calculatedHex); return false; } From cb596f995b029cc59cc6bfec7cee2f6465fed981 Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Mon, 25 May 2026 20:44:13 +0500 Subject: [PATCH 3/5] Update changelog for version 2.0.9 and modify plugin version in marketPayDataHelper.js --- CHANGELOG.md | 5 +++++ .../cartridge/scripts/helpers/marketPayDataHelper.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3244a..389541a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.0.9] + +### Added +- Add signature verification to enhance the callbacks security. + ## [2.0.8] ### Added diff --git a/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPayDataHelper.js b/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPayDataHelper.js index 44ac947..3452ee8 100644 --- a/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPayDataHelper.js +++ b/cartridges/int_marketpay_headless/cartridge/scripts/helpers/marketPayDataHelper.js @@ -258,7 +258,7 @@ function getSessionDataModel() { transactionInfo: { ecomPlatform: "Salesforce", ecomPluginName: "int_marketpay_headless", - ecomPluginVersion: "2.0.8" + ecomPluginVersion: "2.0.9" } }, callbacks: { From 769cc84b63a00ab36a2869ab636d14e91bf80466 Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Mon, 25 May 2026 21:14:11 +0500 Subject: [PATCH 4/5] Remove request validation from CallbackForm --- .../int_marketpay_headless/cartridge/controllers/MarketPay.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js b/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js index bb69139..cb81cdf 100644 --- a/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js +++ b/cartridges/int_marketpay_headless/cartridge/controllers/MarketPay.js @@ -12,9 +12,6 @@ var notificationHelpers = require('*/cartridge/scripts/helpers/marketPayNotifica var signatureHelpers = require('*/cartridge/scripts/helpers/marketPaySignatureHelpers.js'); server.post('CallbackForm', server.middleware.https, function (req, res, next) { - if (!signatureHelpers.validateRequest(req)) { - throw new Error("Invalid request."); - } var languageCode = req.form.language; var formTemplateClass = req.form.form_template; From 698209b4357f12b76b1cec63cea53d6ec48dfb3e Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Tue, 26 May 2026 15:51:25 +0500 Subject: [PATCH 5/5] Filter out MarketPay methods on error. --- .../scripts/hooks/basketPaymentHooks.js | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/cartridges/int_marketpay_headless/cartridge/scripts/hooks/basketPaymentHooks.js b/cartridges/int_marketpay_headless/cartridge/scripts/hooks/basketPaymentHooks.js index 5bdaf9d..f7ba7ee 100644 --- a/cartridges/int_marketpay_headless/cartridge/scripts/hooks/basketPaymentHooks.js +++ b/cartridges/int_marketpay_headless/cartridge/scripts/hooks/basketPaymentHooks.js @@ -9,6 +9,23 @@ const SCAPIService = require('*/cartridge/scripts/services/scapiService'); exports.modifyGETResponse_v2 = function (basket, paymentMethodResultResponse) { const marketPayDataHelper = require('*/cartridge/scripts/helpers/marketPayDataHelper'); + // MarketPay specific payment method IDs to validate + var marketPayMethods = [ + 'MARKETPAY_CREDITCARD', + 'MARKETPAY_MOBILEPAY', + 'MARKETPAY_VIPPS', + 'MARKETPAY_KLARNA', + 'MARKETPAY_IDEAL', + 'MARKETPAY_VIABILL', + 'MARKETPAY_SWISH', + 'MARKETPAY_BANCONTACT', + 'MARKETPAY_BANKPAYMENT', + 'MARKETPAY_TWINT', + 'MARKETPAY_TRUSTLY', + 'MARKETPAY_PRZELEWY24', + 'MARKETPAY_PAYPAL', + ]; + try { var result = SCAPIService.createMarketPaySession(basket.customer.ID, marketPayDataHelper.getFormattedDataForMarketPaySession(basket)); @@ -29,23 +46,6 @@ exports.modifyGETResponse_v2 = function (basket, paymentMethodResultResponse) { marketPayTerminalsMapping = JSON.parse(marketPayTerminalsMapping); } - // MarketPay specific payment method IDs to validate - var marketPayMethods = [ - 'MARKETPAY_CREDITCARD', - 'MARKETPAY_MOBILEPAY', - 'MARKETPAY_VIPPS', - 'MARKETPAY_KLARNA', - 'MARKETPAY_IDEAL', - 'MARKETPAY_VIABILL', - 'MARKETPAY_SWISH', - 'MARKETPAY_BANCONTACT', - 'MARKETPAY_BANKPAYMENT', - 'MARKETPAY_TWINT', - 'MARKETPAY_TRUSTLY', - 'MARKETPAY_PRZELEWY24', - 'MARKETPAY_PAYPAL', - ]; - // Check if marketPayTerminalsMapping is valid if (!marketPayTerminalsMapping || !marketPayTerminalsMapping.terminals) { Logger.warn("MarketPay terminals mapping not found or invalid"); @@ -84,8 +84,13 @@ exports.modifyGETResponse_v2 = function (basket, paymentMethodResultResponse) { Logger.error("MarketPay error in modifyGETResponse_v2: " + e.message); Logger.error("Stack trace: " + e.stack); - // Return original payment methods on error - return; + // Filter out MarketPay methods on error. + var allMethods = paymentMethodResultResponse.applicablePaymentMethods; + if (allMethods) { + paymentMethodResultResponse.applicablePaymentMethods = allMethods.toArray().filter(function (method) { + return marketPayMethods.indexOf(method.id) === -1; + }); + } } };