mirror of
https://github.com/Wolvan/poll.horse.git
synced 2024-11-25 06:07:58 +01:00
Prevent cross site scripting attacks
This commit is contained in:
parent
aee9ed796e
commit
f403165f76
2 changed files with 29 additions and 11 deletions
|
@ -13,6 +13,15 @@ function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKL
|
||||||
return result;
|
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<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;
|
||||||
|
|
|
@ -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, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
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("");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue