diff --git a/config/config.js b/config/config.js index 5666176..2a5d8c1 100644 --- a/config/config.js +++ b/config/config.js @@ -4,6 +4,7 @@ const dev = { apiEndpoint: 'https://apitest.authorize.net/rest/v1', apiLoginId: process.env.apiLogin || 'your api login id', transactionKey: process.env.transactionKey || 'your transaction key', + webhookSecret: process.env.WEBHOOK_SECRET || 'your-webhook-secret-key', app: { port: parseInt(process.env.PORT) || 9000, host: process.env.APP_DB_HOST || '0.0.0.0' @@ -26,6 +27,7 @@ const test = { apiEndpoint: 'https://apitest.authorize.net/rest/v1', apiLoginId: process.env.apiLogin || 'your api login id', transactionKey: process.env.transactionKey || 'your transaction key', + webhookSecret: process.env.WEBHOOK_SECRET || 'your-webhook-secret-key', app: { port: parseInt(process.env.PORT) || 9000, host: process.env.APP_DB_HOST || '0.0.0.0' diff --git a/public/javascripts/index.js b/public/javascripts/index.js index 9438bad..f675412 100644 --- a/public/javascripts/index.js +++ b/public/javascripts/index.js @@ -2,6 +2,21 @@ // Initialize socket variable var socket = io(), maxNotificationCount; +/** + * Escapes HTML special characters to prevent XSS attacks + * @param {string} str - The string to escape + * @returns {string} The escaped string + */ +function escapeHtml(str) { + if (str == null) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + /** * Triggered when an item is dropdown is selected. * Makes current panel content empty @@ -176,12 +191,17 @@ function displayEventMessage(eventDetails) { var formatedPayload = JSON.stringify(eventDetails, null,"\t") .slice(1,-1); + // Escape all user-controlled data to prevent XSS attacks + var safeEventDate = escapeHtml(eventDate); + var safeEventType = escapeHtml(eventDetails.eventType); + var safeFormatedPayload = escapeHtml(formatedPayload); + newPanel.innerHTML = `
-
${eventDate}
-
${eventDetails.eventType}
+
${safeEventDate}
+
${safeEventType}
-
${formatedPayload}
+
${safeFormatedPayload}
`; diff --git a/server.js b/server.js index edb41fb..7c573a0 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ const csurf = require('csurf'); const cookieParser = require("cookie-parser"); var csp = require("helmet-csp"); var fs = require("fs"); +var crypto = require("crypto"); app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, 'public'))); @@ -23,7 +24,7 @@ app.use(csp({ // Specify directives as normal. directives: { defaultSrc: ["'self'"], - scriptSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], fontSrc:["'self'"], imgSrc: ["'self'"], @@ -78,16 +79,53 @@ io.on('connection', function(){ }); }); +/** + * Verifies the HMAC signature of incoming webhook requests + * @param {string} payload - The raw request body + * @param {string} signature - The signature from the request header + * @returns {boolean} - True if signature is valid, false otherwise + */ +function verifyWebhookSignature(payload, signature) { + if (!signature || !config.webhookSecret) { + return false; + } + const expectedSignature = crypto + .createHmac('sha256', config.webhookSecret) + .update(payload) + .digest('hex'); + // Use timing-safe comparison to prevent timing attacks + try { + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } catch (err) { + return false; + } +} + // Handles POST message to "/notifications" endpoint. Updates notifications set, inserts // new notification, emit a new event to be captured by front end code -app.post("/notifications", async function (req, res) { +app.post("/notifications", express.text({ type: '*/*' }), async function (req, res) { try { + // Verify webhook signature (HMAC) + const signature = req.get('X-ANET-Signature') || req.get('X-Webhook-Signature'); + const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body); + + if (!verifyWebhookSignature(rawBody, signature)) { + console.error('Webhook signature verification failed'); + return res.status(401).json({ error: 'Invalid webhook signature' }); + } + + // Parse body if it was received as text + const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body; + io.emit("newNotification", { - eventDetails: (req.body), + eventDetails: body, }); var notifications = await updateNotifications(); - notifications = await insertNewNotification(notifications, req.body); + notifications = await insertNewNotification(notifications, body); res.sendStatus(201); }catch(err) {