Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
26 changes: 23 additions & 3 deletions public/javascripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

/**
* Triggered when an item is dropdown is selected.
* Makes current panel content empty
Expand Down Expand Up @@ -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 =
`<div class="row event-headings" style="background-color: lavender;height: 100px">
<div class="col-xs-2">${eventDate}</div>
<div class="col-xs-5" style="padding: 0px;text-align: center;word-break: break-all">${eventDetails.eventType}</div>
<div class="col-xs-2">${safeEventDate}</div>
<div class="col-xs-5" style="padding: 0px;text-align: center;word-break: break-all">${safeEventType}</div>
<div class="col-xs-5" style="padding: 0px;">
<pre style="background-color: palegoldenrod;padding: 0px;margin: 0px auto;height: 100px">${formatedPayload}</pre>
<pre style="background-color: palegoldenrod;padding: 0px;margin: 0px auto;height: 100px">${safeFormatedPayload}</pre>
</div>
</div>`;

Expand Down
46 changes: 42 additions & 4 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
Expand All @@ -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'"],
Expand Down Expand Up @@ -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) {
Expand Down