From f403165f76c6d3844e6f4f57efe92185abfb2b6d Mon Sep 17 00:00:00 2001 From: Wolvan Date: Sat, 8 Jan 2022 16:16:40 +0100 Subject: [PATCH] Prevent cross site scripting attacks --- src/backend.ts | 15 ++++++++++++--- src/frontend.ts | 25 +++++++++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/backend.ts b/src/backend.ts index 43c5979..dbc30f9 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -13,6 +13,15 @@ function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKL return result; } +function unxss(str: string) { + return str + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, "\"") + .replace(/'/g, "'"); +} + export default async function init(router: Router, polls: Storage): Promise { router.get("/poll/:id", async (req, res) => { try { @@ -171,11 +180,11 @@ export default async function init(router: Router, polls: Storage): Promise i && possibleVotes.includes(i)).length < 1) return { + if (!Array.isArray(votes) || votes.filter(i => i && possibleVotes.includes(unxss(i))).length < 1) return { error: "Votes must be an array and have at least 1 entry", statusCode: 400 }; - if (!poll.multiSelect && votes.filter(i => i && possibleVotes.includes(i)).length > 1) return { + if (!poll.multiSelect && votes.filter(i => i && possibleVotes.includes(unxss(i))).length > 1) return { error: "Single-select polls can only have one vote", statusCode: 400 }; @@ -191,7 +200,7 @@ export default async function init(router: Router, polls: Storage): Promise i && possibleVotes.includes(i)).forEach(vote => poll.options[vote]++); + votes.filter(i => i && possibleVotes.includes(unxss(i))).forEach(vote => poll.options[unxss(vote)]++); await polls.setItem(pollId, poll); return null; diff --git a/src/frontend.ts b/src/frontend.ts index 832ca29..a55dd73 100644 --- a/src/frontend.ts +++ b/src/frontend.ts @@ -95,6 +95,15 @@ async function displayPage(req: Request, res: Response, htmlFilename: string, re } } +function xss(unsafe: string) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + export default function init(router: Router): void { router.get("/:id/r", async (req, res) => { const id = req.params.id; @@ -105,9 +114,9 @@ export default function init(router: Router): void { if (!poll || poll.error) return res.redirect("/"); const totalVotes = Object.values(poll.votes).reduce((acc, cur) => acc + cur, 0); const pollOptionsDivs = Object.entries(poll.votes).map(([option, votes]) => ` -
+
-
${ option }
${ votes }
+
${ xss(option) }
${ votes }
@@ -119,7 +128,7 @@ export default function init(router: Router): void { await displayPage(req, res, "result.html", { "POLL_ID": id, - "POLL_TITLE": poll.title, + "POLL_TITLE": xss(poll.title), "POLL_OPTION_DIVS": pollOptionsDivs, "POLL_VOTES_TOTAL": totalVotes, "BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""), @@ -142,17 +151,17 @@ export default function init(router: Router): void { const pollOptions = poll.options.map(option => `
-
-
${option}
+
+
${xss(option)}
` ).join(""); await displayPage(req, res, "poll.html", { "POLL_ID": poll.id, - "POLL_TITLE": poll.title, + "POLL_TITLE": xss(poll.title), "POLL_OPTION_DIVS": pollOptions, "BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""), - "FORM_SUBMISSION_ERROR": req.query.error, + "FORM_SUBMISSION_ERROR": xss(req.query.error + ""), "FORM_SUBMISSION_ERROR_SHOWN_CLASS": req.query.error ? "error-visible" : "", }); } catch (error) { @@ -168,7 +177,7 @@ export default function init(router: Router): void { .slice(0, 3); const pollOptionDivs = options.map(option => `
- +
`).join("");