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) {