Prevent cross site scripting attacks

This commit is contained in:
Wolvan 2022-01-08 16:16:40 +01:00
parent aee9ed796e
commit f403165f76
2 changed files with 29 additions and 11 deletions

View file

@ -13,6 +13,15 @@ function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKL
return result; return result;
} }
function unxss(str: string) {
return str
.replace(/&/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, "\"")
.replace(/&#039;/g, "'");
}
export default async function init(router: Router, polls: Storage): Promise<void> { export default async function init(router: Router, polls: Storage): Promise<void> {
router.get("/poll/:id", async (req, res) => { router.get("/poll/:id", async (req, res) => {
try { try {
@ -171,11 +180,11 @@ export default async function init(router: Router, polls: Storage): Promise<void
}; };
const possibleVotes = Object.keys(poll.options); const possibleVotes = Object.keys(poll.options);
if (!Array.isArray(votes) || votes.filter(i => 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", error: "Votes must be an array and have at least 1 entry",
statusCode: 400 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", error: "Single-select polls can only have one vote",
statusCode: 400 statusCode: 400
}; };
@ -191,7 +200,7 @@ export default async function init(router: Router, polls: Storage): Promise<void
maxAge: (1000 * 60 * 60 * 24 * 365) / 2 maxAge: (1000 * 60 * 60 * 24 * 365) / 2
}); });
} }
votes.filter(i => 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); await polls.setItem(pollId, poll);
return null; return null;

View file

@ -95,6 +95,15 @@ async function displayPage(req: Request, res: Response, htmlFilename: string, re
} }
} }
function xss(unsafe: string) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
export default function init(router: Router): void { export default function init(router: Router): void {
router.get("/:id/r", async (req, res) => { router.get("/:id/r", async (req, res) => {
const id = req.params.id; const id = req.params.id;
@ -105,9 +114,9 @@ export default function init(router: Router): void {
if (!poll || poll.error) return res.redirect("/"); if (!poll || poll.error) return res.redirect("/");
const totalVotes = Object.values(poll.votes).reduce((acc, cur) => acc + cur, 0); const totalVotes = Object.values(poll.votes).reduce((acc, cur) => acc + cur, 0);
const pollOptionsDivs = Object.entries(poll.votes).map(([option, votes]) => ` const pollOptionsDivs = Object.entries(poll.votes).map(([option, votes]) => `
<div class="poll-option" option="${ option }"> <div class="poll-option" option="${ xss(option) }">
<div class="poll-option-info"> <div class="poll-option-info">
<div class="poll-option-text">${ option }</div><div class="poll-option-votes">${ votes }</div> <div class="poll-option-text">${ xss(option) }</div><div class="poll-option-votes">${ votes }</div>
</div> </div>
<div class="progress"> <div class="progress">
<div class="poll-bar"> <div class="poll-bar">
@ -119,7 +128,7 @@ export default function init(router: Router): void {
await displayPage(req, res, "result.html", { await displayPage(req, res, "result.html", {
"POLL_ID": id, "POLL_ID": id,
"POLL_TITLE": poll.title, "POLL_TITLE": xss(poll.title),
"POLL_OPTION_DIVS": pollOptionsDivs, "POLL_OPTION_DIVS": pollOptionsDivs,
"POLL_VOTES_TOTAL": totalVotes, "POLL_VOTES_TOTAL": totalVotes,
"BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""), "BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""),
@ -142,17 +151,17 @@ export default function init(router: Router): void {
const pollOptions = poll.options.map(option => const pollOptions = poll.options.map(option =>
`<div class="poll-option"> `<div class="poll-option">
<div class="input-container"> <div class="input-container">
<input type="${poll.multiSelect ? "checkbox" : "radio"}" name="poll-option" value="${option}"" ${options.includes(option) ? "checked" : ""}/><div class="checkmark"><svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg></div> <input type="${poll.multiSelect ? "checkbox" : "radio"}" name="poll-option" value="${xss(option)}"" ${options.includes(option) ? "checked" : ""}/><div class="checkmark"><svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/></svg></div>
</div><div class="text">${option}</div> </div><div class="text">${xss(option)}</div>
</div>` </div>`
).join(""); ).join("");
await displayPage(req, res, "poll.html", { await displayPage(req, res, "poll.html", {
"POLL_ID": poll.id, "POLL_ID": poll.id,
"POLL_TITLE": poll.title, "POLL_TITLE": xss(poll.title),
"POLL_OPTION_DIVS": pollOptions, "POLL_OPTION_DIVS": pollOptions,
"BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""), "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" : "", "FORM_SUBMISSION_ERROR_SHOWN_CLASS": req.query.error ? "error-visible" : "",
}); });
} catch (error) { } catch (error) {
@ -168,7 +177,7 @@ export default function init(router: Router): void {
.slice(0, 3); .slice(0, 3);
const pollOptionDivs = options.map(option => ` const pollOptionDivs = options.map(option => `
<div class="poll-option"> <div class="poll-option">
<input type="text" name="poll-option" maxlength="${MAX_CHARACTER_LENGTH}" placeholder="Enter your option here" value="${option}"> <input type="text" name="poll-option" maxlength="${MAX_CHARACTER_LENGTH}" placeholder="Enter your option here" value="${xss(option)}">
</div> </div>
`).join(""); `).join("");