working on xss mitigation
This commit is contained in:
parent
9fee4b533d
commit
c1322466b1
@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/',
|
||||
};
|
||||
const reg = /[&<>"'/]/ig;
|
||||
return string.replace(reg, (match) => map[match]);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Activate all tooltips
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
|
@ -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;"> </div>`
|
||||
const embedColour = sanitise(row.subscription.embed_colour);
|
||||
return `<div class="h-100" style="background-color: #${embedColour}; width: .25rem;"> </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
|
||||
);
|
||||
|
@ -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
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>");
|
||||
|
@ -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`);
|
||||
|
||||
}
|
||||
|
@ -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;"> </div>`
|
||||
const embedColour = sanitise(row.embed_colour);
|
||||
return `<div class="h-100" style="background-color: #${embedColour}; width: .25rem;"> </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) {
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user