working on xss mitigation

This commit is contained in:
Corban-Lee Jones 2024-09-12 00:12:29 +01:00
parent 9fee4b533d
commit c1322466b1
7 changed files with 116 additions and 67 deletions

View File

@ -19,6 +19,24 @@ function getCurrentDateTime() {
return `${year}-${month}-${day}T${hours}:${minutes}`; return `${year}-${month}-${day}T${hours}:${minutes}`;
} }
// Sanitise a given string to remove HTML, making it DOM safe.
function sanitise(string) {
if (typeof string !== "string") {
return string;
}
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
"/": '&#x2F;',
};
const reg = /[&<>"'/]/ig;
return string.replace(reg, (match) => map[match]);
}
$(document).ready(function() { $(document).ready(function() {
// Activate all tooltips // Activate all tooltips
$('[data-bs-toggle="tooltip"]').tooltip(); $('[data-bs-toggle="tooltip"]').tooltip();

View File

@ -46,7 +46,9 @@ async function initContentTable() {
data: "title", data: "title",
className: "text-truncate", className: "text-truncate",
render: function(data, type, row) { render: function(data, type, row) {
return `<a href="${row.url}" class="btn btn-link text-start text-decoration-none" target="_blank">${data}</a>` const title = sanitise(data);
const url = sanitise(row.url);
return `<a href="${url}" class="btn btn-link text-start text-decoration-none" target="_blank">${title}</a>`
} }
}, },
{ {
@ -54,7 +56,8 @@ async function initContentTable() {
data: "subscription.name", data: "subscription.name",
className: "text-nowrap", className: "text-nowrap",
render: function(data, type, row) { render: function(data, type, row) {
return `<button type="button" onclick="goToSubscription(${row.subscription.id})" class="btn btn-link text-start text-decoration-none">${data}</button>` const subName = sanitise(data);
return `<button type="button" onclick="goToSubscription(${row.subscription.id})" class="btn btn-link text-start text-decoration-none">${subName}</button>`
} }
}, },
{ {
@ -70,7 +73,9 @@ async function initContentTable() {
data: "channel_id", data: "channel_id",
className: "text-start", className: "text-start",
render: function(data, type, row) { render: function(data, type, row) {
return `<div class="resolve-channel-name text-center" data-channel-id="${data}" data-msg-id="${row.message_id}"> const channelId = sanitise(data);
const messageId = sanitise(row.message_id);
return `<div class="resolve-channel-name text-center" data-channel-id="${channelId}" data-msg-id="${messageId}">
<div class="spinner-border spinner-border-sm" role="status"> <div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
@ -98,7 +103,8 @@ async function initContentTable() {
orderable: false, orderable: false,
className: "p-0", className: "p-0",
render: function(data, type, row) { render: function(data, type, row) {
return `<div class="h-100" style="background-color: #${row.subscription.embed_colour}; width: .25rem;">&nbsp;</div>` const embedColour = sanitise(row.subscription.embed_colour);
return `<div class="h-100" style="background-color: #${embedColour}; width: .25rem;">&nbsp;</div>`
} }
} }
] ]
@ -166,17 +172,18 @@ async function deleteSelectedContent() {
const rows = contentTable.rows(".selected").data().toArray(); const rows = contentTable.rows(".selected").data().toArray();
const names = rows.map(row => { return row.title }); const names = rows.map(row => { return row.title });
const namesString = arrayToHtmlList(names, true).prop("outerHTML"); const namesString = arrayToHtmlList(names, true).prop("outerHTML");
const multiple = names.length > 1; const isMany = names.length > 1;
await confirmDeleteModal( await confirmationModal(
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`, `Delete ${isMany ? "Many Tracked Contents" : "a Tracked Content"}`,
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> content${multiple ? "s" : ""}?<br><br>${namesString}`, `Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> Tracked Content${isMany ? "s" : ""}?<br><br>${namesString}`,
"danger",
async () => { async () => {
rows.forEach(async row => { await deleteTrackedContent(row.id) }); rows.forEach(async row => { await deleteTrackedContent(row.id) });
showToast( showToast(
"danger", "danger",
`Deleted ${names.length} Content${multiple ? "s" : ""}`, `Deleted ${names.length} Content${isMany ? "s" : ""}`,
`${arrayToHtmlList(names, false).prop("outerHTML")}`, `${arrayToHtmlList(names, false).prop("outerHTML")}`,
12000 12000
); );

View File

@ -40,7 +40,8 @@ async function initFiltersTable() {
title: "Name", title: "Name",
data: "name", data: "name",
render: function(data, type, row) { render: function(data, type, row) {
return `<button type="button" onclick="showEditFilterModal(${row.id})" class="btn btn-link text-start text-decoration-none">${data}</button>` const name = sanitise(data);
return `<button type="button" onclick="showEditFilterModal(${row.id})" class="btn btn-link text-start text-decoration-none">${name}</button>`
} }
}, },
{ {
@ -55,7 +56,7 @@ async function initFiltersTable() {
case 5: return "Fuzzy Match"; case 5: return "Fuzzy Match";
default: default:
console.error(`unknown matching algorithm '${data}'`); console.error(`unknown matching algorithm '${data}'`);
return data; return sanitise(data);
} }
} }
}, },
@ -252,12 +253,14 @@ $(document).on("selectedServerChange", async function() {
$("#deleteEditFilter").on("click", async function() { $("#deleteEditFilter").on("click", async function() {
const filterId = parseInt($("#filterId").val()); const filterId = parseInt($("#filterId").val());
const filter = filtersTable.row(function(idx, row) { return row.id === filterId }).data(); const filter = filtersTable.row(function(idx, row) { return row.id === filterId }).data();
const filterName = sanitise(filter.name);
$("#filterFormModal").modal("hide"); $("#filterFormModal").modal("hide");
await confirmDeleteModal( await confirmationModal(
"Confirm Deletion", "Delete a Filter",
`Do you wish to permanently delete <b>${filter.name}</b>?`, // FIX: potential xss attack `Do you wish to permanently delete <b>${filterName}</b>?`,
"danger",
async () => { async () => {
await deleteFilter(filterId); await deleteFilter(filterId);
await loadFilters(getCurrentlyActiveServer().guild_id); await loadFilters(getCurrentlyActiveServer().guild_id);
@ -265,7 +268,7 @@ $("#deleteEditFilter").on("click", async function() {
showToast( showToast(
"danger", "danger",
"Deleted a Filter", "Deleted a Filter",
filter.name, // FIX: potential xss attack filterName,
12000 12000
); );
}, },
@ -279,27 +282,29 @@ async function deleteSelectedFilters() {
const rows = filtersTable.rows(".selected").data().toArray(); const rows = filtersTable.rows(".selected").data().toArray();
const names = rows.map(row => row.name); const names = rows.map(row => row.name);
const namesString = arrayToHtmlList(names, true).prop("outerHTML"); const namesString = arrayToHtmlList(names, true).prop("outerHTML");
const multiple = names.length > 1; const isMany = names.length > 1;
await confirmDeleteModal( await confirmationModal(
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`, `Delete ${isMany ? "Many Filters" : "a Filter"}`,
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> filter${multiple ? "s" : ""}?<br><br>${namesString}`, `Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> filter${isMany ? "s" : ""}?<br><br>${namesString}`,
"danger",
async () => { async () => {
rows.forEach(async row => { await deleteFilter(row.id) }); rows.forEach(async row => { await deleteFilter(row.id) });
showToast( showToast(
"danger", "danger",
`Delete ${names.length} Subscription${multiple ? "s" : ""}`, `Delete ${names.length} Subscription${isMany ? "s" : ""}`,
`${arrayToHtmlList(names, false).prop("outerHTML")}`, `${arrayToHtmlList(names, false).prop("outerHTML")}`,
12000 12000
); );
// Multi-deletion can take time, this timeout ensures the refresh is accurate // Multi-deletion can take time, this timeout ensures the refresh is accurate
setTimeout(async () => { setTimeout(async () => {
await loadFilters(getCurrentlyActiveServer().guild_id); await loadFilters(getCurrentlyActiveServer().guild_id);
}, 600); }, 600);
}, },
null null
); );
} }

View File

@ -155,23 +155,33 @@ $(document).ready(function() {
}); });
}); });
async function confirmDeleteModal(title, description, acceptFunc, declineFunc) { async function confirmationModal(title, bodyText, style, acceptFunc, declineFunc) {
let $modal = $("#confirmDeleteModal"); let $modal = $("#confirmationModal");
// Ensure valid style and apply it to the confirm button
if (!["danger", "success", "warning", "info", "primary", "secondary"].includes(style)) {
throw new Error(`${style} is not a valid style`);
}
$modal.find(".modal-confirm-btn").addClass(`btn-${style}`);
$modal.find(".modal-title").text(title); $modal.find(".modal-title").text(title);
$modal.find(".modal-body > p").html(description); $modal.find(".modal-body > p").html(bodyText);
$modal.find(".confirm-delete-btn").off("click").on("click", async function(e) {
$modal.find(".modal-confirm-btn").off("click").on("click", async function(e) {
await acceptFunc() await acceptFunc()
$modal.modal("hide"); $modal.modal("hide");
}); });
$modal.find(".dismiss-delete-btn").off("click").on("click", async function(e) {
$modal.find(".modal-dismiss-btn").off("click").on("click", async function(e) {
if (declineFunc) await declineFunc(); if (declineFunc) await declineFunc();
$modal.modal("hide"); $modal.modal("hide");
}); });
$modal.modal("show"); $modal.modal("show");
} }
function arrayToHtmlList(array, bold=false) { function arrayToHtmlList(array, bold=false) {
$ul = $("<ul>"); $ul = $("<ul>").addClass("mb-0");
array.forEach(item => { array.forEach(item => {
let $li = $("<li>"); let $li = $("<li>");

View File

@ -35,7 +35,7 @@ function addToLoadedServers(server, selectNew=true) {
loadedServers[id] = server; loadedServers[id] = server;
// Display the loaded server // Display the loaded server
addServerTemplate(id, server.guild_id, server.name, server.icon, server.permissions, server.owner); addServerTemplate(id, sanitise(server.guild_id), sanitise(server.name), sanitise(server.icon), sanitise(server.permissions), sanitise(server.owner));
// Select the newly added server // Select the newly added server
if (selectNew) { if (selectNew) {
@ -90,10 +90,10 @@ async function loadServerOptions() {
servers.forEach(server => { servers.forEach(server => {
$("#serverOptions").append($("<option>", { $("#serverOptions").append($("<option>", {
value: server.id, value: server.id,
text: server.name, text: sanitise(server.name),
"data-icon": server.icon, "data-icon": sanitise(server.icon),
"data-permissions": server.permissions, "data-permissions": sanitise(server.permissions),
"data-isowner": server.owner "data-isowner": sanitise(server.owner)
})); }));
}); });
} }
@ -199,7 +199,7 @@ async function registerNewServer(serverName, serverGuildId, serverIconHash, serv
try { response = await newSavedGuild(formData); } try { response = await newSavedGuild(formData); }
catch (err) { catch (err) {
if (err.status === 409) if (err.status === 409)
showToast("warning", "Server Conflict", `Can't add ${serverName} because it already exists.`, 10000); showToast("warning", "Server Conflict", `Can't add ${sanitise(serverName)} because it already exists.`, 10000);
else else
console.error(JSON.stringify(err, null, 4)); console.error(JSON.stringify(err, null, 4));
@ -221,8 +221,8 @@ function selectServer(primaryKey) {
$(`#serverList .server-item[data-id=${primaryKey}]`).addClass("active") $(`#serverList .server-item[data-id=${primaryKey}]`).addClass("active")
// Display details of the selected server // Display details of the selected server
$("#selectedServerContainer .selected-server-name").text(server.name); $("#selectedServerContainer .selected-server-name").text(sanitise(server.name));
$("#selectedServerContainer .selected-server-id").text(server.guild_id); $("#selectedServerContainer .selected-server-id").text(sanitise(server.guild_id));
$("#selectedServerContainer .selected-server-icon").attr("src", `https://cdn.discordapp.com/icons/${server.guild_id}/${server.icon}.webp?size=80`); $("#selectedServerContainer .selected-server-icon").attr("src", `https://cdn.discordapp.com/icons/${server.guild_id}/${server.icon}.webp?size=80`);
// Disable all loaded servers // Disable all loaded servers
@ -253,9 +253,10 @@ $("#deleteSelectedServerBtn").on("click", async function() {
]; ];
const notesString = arrayToHtmlList(notes).prop("outerHTML"); const notesString = arrayToHtmlList(notes).prop("outerHTML");
await confirmDeleteModal( await confirmationModal(
"Close this server?", "Close this server?",
`This is a safe, non-permanent action:<br><br>${notesString}`, `This is a safe, non-permanent action:<br><br>${notesString}`,
"warning",
deleteSelectedServer, deleteSelectedServer,
null null
); );
@ -294,17 +295,17 @@ function resolveServerStrings() {
const server = getCurrentlyActiveServer(); const server = getCurrentlyActiveServer();
// Server names // Server names
$(".resolve-to-server-name").text(server.name); $(".resolve-to-server-name").text(sanitise(server.name));
// Server Guild Ids // Server Guild Ids
$(".resolve-to-server-id").text(server.guild_id) $(".resolve-to-server-id").text(sanitise(server.guild_id))
// Bot Invite links // Bot Invite links
$(".resolve-to-invite-link").attr("href", `https://discord.com/oauth2/authorize $(".resolve-to-invite-link").attr("href", `https://discord.com/oauth2/authorize
?client_id=${discordClientId} ?client_id=${discordClientId}
&permissions=2147534848 &permissions=2147534848
&scope=bot+applications.commands &scope=bot+applications.commands
&guild_id=${server.guild_id} &guild_id=${sanitise(server.guild_id)}
&disable_guild_select=true`); &disable_guild_select=true`);
} }

View File

@ -43,7 +43,8 @@ async function initSubscriptionTable() {
data: "name", data: "name",
className: "text-truncate", className: "text-truncate",
render: function(data, type, row) { render: function(data, type, row) {
return `<button type="button" onclick="showEditSubModal(${row.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`; const name = sanitise(data);
return `<button type="button" onclick="showEditSubModal(${row.id})" class="btn btn-link text-start text-decoration-none">${name}</button>`;
} }
}, },
{ {
@ -51,7 +52,8 @@ async function initSubscriptionTable() {
data: "url", data: "url",
className: "text-truncate", className: "text-truncate",
render: function(data, type) { render: function(data, type) {
return `<a href="${data}" class="btn btn-link text-start text-decoration-none" target="_blank">${data}</a>`; const url = sanitise(data);
return `<a href="${url}" class="btn btn-link text-start text-decoration-none" target="_blank">${url}</a>`;
} }
}, },
{ {
@ -59,7 +61,8 @@ async function initSubscriptionTable() {
data: "channels_count", data: "channels_count",
className: "text-center", className: "text-center",
render: function(data) { render: function(data) {
return `<span class="badge text-bg-secondary">${data}</span>`; const channelsCount = sanitise(data);
return `<span class="badge text-bg-secondary">${channelsCount}</span>`;
} }
}, },
{ {
@ -84,13 +87,14 @@ async function initSubscriptionTable() {
orderable: false, orderable: false,
className: "text-center", className: "text-center",
render: function(data, type) { render: function(data, type) {
if (!data) return ""; if (!data) { return "" }
const extraNotes = sanitise(data);
return $(` return $(`
<i class="bi bi-chat-left-text" <i class="bi bi-chat-left-text"
data-bs-trigger="hover focus" data-bs-trigger="hover focus"
data-bs-toggle="popover" data-bs-toggle="popover"
data-bs-title="Extra Notes" data-bs-title="Extra Notes"
data-bs-content="${data}"> data-bs-content="${extraNotes}">
</i> </i>
`).popover()[0]; `).popover()[0];
} }
@ -108,7 +112,8 @@ async function initSubscriptionTable() {
orderable: false, orderable: false,
className: "p-0", className: "p-0",
render: function(data, type, row) { render: function(data, type, row) {
return `<div class="h-100" style="background-color: #${row.embed_colour}; width: .25rem;">&nbsp;</div>` const embedColour = sanitise(row.embed_colour);
return `<div class="h-100" style="background-color: #${embedColour}; width: .25rem;">&nbsp;</div>`
} }
} }
] ]
@ -429,20 +434,22 @@ async function updateDefaultSubEmbedColour(settings=null) {
$("#deleteEditSub").on("click", async function() { $("#deleteEditSub").on("click", async function() {
const subId = parseInt($("#subId").val()); const subId = parseInt($("#subId").val());
const sub = subTable.row(function(idx, row) { return row.id === subId }).data(); const sub = subTable.row(function(idx, row) { return row.id === subId }).data();
const subName = sanitise(sub.name);
$("#subFormModal").modal("hide"); $("#subFormModal").modal("hide");
await confirmDeleteModal( await confirmationModal(
"Confirm Deletion", "Delete a Subscription",
`Do you wish to permanently delete <b>${sub.name}</b>?`, // FIX: potential xss attack `Do you wish to permanently delete <b>${subName}</b>?`,
"danger",
async () => { async () => {
await deleteSubscription(subId); await deleteSubscription(subId);
await loadSubscriptions(getCurrentlyActiveServer().guild_id); await loadSubscriptions(getCurrentlyActiveServer().guild_id);
showToast( showToast(
"danger", "danger",
"Deleted a Subscription", "Deleted a Subscription",
sub.name, // FIX: potential xss attack subName,
12000 12000
); );
}, },
@ -456,34 +463,35 @@ async function deleteSelectedSubscriptions() {
const rows = subTable.rows(".selected").data().toArray(); const rows = subTable.rows(".selected").data().toArray();
const names = rows.map(row => row.name); const names = rows.map(row => row.name);
const namesString = arrayToHtmlList(names, true).prop("outerHTML"); const namesString = arrayToHtmlList(names, true).prop("outerHTML");
const multiple = names.length > 1; const isMany = names.length > 1;
await confirmDeleteModal( await confirmationModal(
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`, `Delete ${isMany ? "Many Subscriptions" : "a Subscription"}`,
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> subscription${multiple ? "s" : ""}?<br><br>${namesString}`, `Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> subscription${isMany ? "s" : ""}?<br><br>${namesString}`,
"danger",
async () => { async () => {
rows.forEach(async row => { await deleteSubscription(row.id) }); rows.forEach(async row => { await deleteSubscription(row.id) });
showToast( showToast(
"danger", "danger",
`Deleted ${names.length} Subscription${multiple ? "s" : ""}`, `Deleted ${names.length} Subscription${isMany ? "s" : ""}`,
`${arrayToHtmlList(names, false).prop("outerHTML")}`, `${arrayToHtmlList(names, false).prop("outerHTML")}`,
12000 12000
) );
// Multi-deletion can take time, this timeout ensures the refresh is accurate // Multi-deletion can take time, this timeout ensures the refresh is accurate
setTimeout(async () => { setTimeout(async () => {
await loadSubscriptions(getCurrentlyActiveServer().guild_id); await loadSubscriptions(getCurrentlyActiveServer().guild_id);
}, 600); }, 600);
}, },
null null
)
);
} }
// #endregion // #endregion
// #region Load Modal Options // #region Load Modal Options
async function loadChannelOptions(guildId) { async function loadChannelOptions(guildId) {

View File

@ -1,15 +1,15 @@
<div id="confirmDeleteModal" class="modal fade" data-bs-backdrop="static" tabindex="-1"> <div id="confirmationModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-1"> <div class="modal-content rounded-1">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title mx-2"></h5> <h5 class="modal-title mx-2"></h5>
</div> </div>
<div class="modal-body"> <div class="modal-body p-4">
<p class="mx-2"></p> <p class="mb-0"></p>
</div> </div>
<div class="modal-footer px-4"> <div class="modal-footer px-4">
<button type="button" class="btn btn-danger rounded-1 confirm-delete-btn" tabindex="1">Delete</button> <button type="button" class="btn rounded-1 modal-confirm-btn" tabindex="1">Confirm</button>
<button type="button" class="btn btn-secondary rounded-1 ms-3 ms-0 dismiss-delete-btn" tabindex="2">Cancel</button> <button type="button" class="btn btn-secondary rounded-1 ms-3 ms-0 modal-dismiss-btn" tabindex="2">Cancel</button>
</div> </div>
</div> </div>
</div> </div>