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}`;
}
// 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() {
// Activate all tooltips
$('[data-bs-toggle="tooltip"]').tooltip();

View File

@ -46,7 +46,9 @@ async function initContentTable() {
data: "title",
className: "text-truncate",
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",
className: "text-nowrap",
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",
className: "text-start",
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">
<span class="visually-hidden">Loading...</span>
</div>
@ -98,7 +103,8 @@ async function initContentTable() {
orderable: false,
className: "p-0",
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 names = rows.map(row => { return row.title });
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
const multiple = names.length > 1;
const isMany = names.length > 1;
await confirmDeleteModal(
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`,
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> content${multiple ? "s" : ""}?<br><br>${namesString}`,
await confirmationModal(
`Delete ${isMany ? "Many Tracked Contents" : "a Tracked Content"}`,
`Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> Tracked Content${isMany ? "s" : ""}?<br><br>${namesString}`,
"danger",
async () => {
rows.forEach(async row => { await deleteTrackedContent(row.id) });
showToast(
"danger",
`Deleted ${names.length} Content${multiple ? "s" : ""}`,
`Deleted ${names.length} Content${isMany ? "s" : ""}`,
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
12000
);

View File

@ -40,7 +40,8 @@ async function initFiltersTable() {
title: "Name",
data: "name",
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";
default:
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() {
const filterId = parseInt($("#filterId").val());
const filter = filtersTable.row(function(idx, row) { return row.id === filterId }).data();
const filterName = sanitise(filter.name);
$("#filterFormModal").modal("hide");
await confirmDeleteModal(
"Confirm Deletion",
`Do you wish to permanently delete <b>${filter.name}</b>?`, // FIX: potential xss attack
await confirmationModal(
"Delete a Filter",
`Do you wish to permanently delete <b>${filterName}</b>?`,
"danger",
async () => {
await deleteFilter(filterId);
await loadFilters(getCurrentlyActiveServer().guild_id);
@ -265,7 +268,7 @@ $("#deleteEditFilter").on("click", async function() {
showToast(
"danger",
"Deleted a Filter",
filter.name, // FIX: potential xss attack
filterName,
12000
);
},
@ -279,27 +282,29 @@ async function deleteSelectedFilters() {
const rows = filtersTable.rows(".selected").data().toArray();
const names = rows.map(row => row.name);
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
const multiple = names.length > 1;
const isMany = names.length > 1;
await confirmDeleteModal(
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`,
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> filter${multiple ? "s" : ""}?<br><br>${namesString}`,
await confirmationModal(
`Delete ${isMany ? "Many Filters" : "a Filter"}`,
`Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> filter${isMany ? "s" : ""}?<br><br>${namesString}`,
"danger",
async () => {
rows.forEach(async row => { await deleteFilter(row.id) });
showToast(
"danger",
`Delete ${names.length} Subscription${multiple ? "s" : ""}`,
`Delete ${names.length} Subscription${isMany ? "s" : ""}`,
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
12000
);
// Multi-deletion can take time, this timeout ensures the refresh is accurate
setTimeout(async () => {
await loadFilters(getCurrentlyActiveServer().guild_id);
}, 600);
},
null
);
}

View File

@ -155,23 +155,33 @@ $(document).ready(function() {
});
});
async function confirmDeleteModal(title, description, acceptFunc, declineFunc) {
let $modal = $("#confirmDeleteModal");
async function confirmationModal(title, bodyText, style, acceptFunc, declineFunc) {
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-body > p").html(description);
$modal.find(".confirm-delete-btn").off("click").on("click", async function(e) {
$modal.find(".modal-body > p").html(bodyText);
$modal.find(".modal-confirm-btn").off("click").on("click", async function(e) {
await acceptFunc()
$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();
$modal.modal("hide");
});
$modal.modal("show");
}
function arrayToHtmlList(array, bold=false) {
$ul = $("<ul>");
$ul = $("<ul>").addClass("mb-0");
array.forEach(item => {
let $li = $("<li>");

View File

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

View File

@ -43,7 +43,8 @@ async function initSubscriptionTable() {
data: "name",
className: "text-truncate",
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",
className: "text-truncate",
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",
className: "text-center",
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,
className: "text-center",
render: function(data, type) {
if (!data) return "";
if (!data) { return "" }
const extraNotes = sanitise(data);
return $(`
<i class="bi bi-chat-left-text"
data-bs-trigger="hover focus"
data-bs-toggle="popover"
data-bs-title="Extra Notes"
data-bs-content="${data}">
data-bs-content="${extraNotes}">
</i>
`).popover()[0];
}
@ -108,7 +112,8 @@ async function initSubscriptionTable() {
orderable: false,
className: "p-0",
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() {
const subId = parseInt($("#subId").val());
const sub = subTable.row(function(idx, row) { return row.id === subId }).data();
const subName = sanitise(sub.name);
$("#subFormModal").modal("hide");
await confirmDeleteModal(
"Confirm Deletion",
`Do you wish to permanently delete <b>${sub.name}</b>?`, // FIX: potential xss attack
await confirmationModal(
"Delete a Subscription",
`Do you wish to permanently delete <b>${subName}</b>?`,
"danger",
async () => {
await deleteSubscription(subId);
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
showToast(
"danger",
"Deleted a Subscription",
sub.name, // FIX: potential xss attack
subName,
12000
);
},
@ -456,34 +463,35 @@ async function deleteSelectedSubscriptions() {
const rows = subTable.rows(".selected").data().toArray();
const names = rows.map(row => row.name);
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
const multiple = names.length > 1;
const isMany = names.length > 1;
await confirmDeleteModal(
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`,
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> subscription${multiple ? "s" : ""}?<br><br>${namesString}`,
await confirmationModal(
`Delete ${isMany ? "Many Subscriptions" : "a Subscription"}`,
`Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> subscription${isMany ? "s" : ""}?<br><br>${namesString}`,
"danger",
async () => {
rows.forEach(async row => { await deleteSubscription(row.id) });
showToast(
"danger",
`Deleted ${names.length} Subscription${multiple ? "s" : ""}`,
`Deleted ${names.length} Subscription${isMany ? "s" : ""}`,
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
12000
)
);
// Multi-deletion can take time, this timeout ensures the refresh is accurate
setTimeout(async () => {
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
}, 600);
},
null
)
);
}
// #endregion
// #region Load Modal Options
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-content rounded-1">
<div class="modal-header">
<h5 class="modal-title mx-2"></h5>
</div>
<div class="modal-body">
<p class="mx-2"></p>
<div class="modal-body p-4">
<p class="mb-0"></p>
</div>
<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 btn-secondary rounded-1 ms-3 ms-0 dismiss-delete-btn" tabindex="2">Cancel</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 modal-dismiss-btn" tabindex="2">Cancel</button>
</div>
</div>
</div>