Use CSRF token to discourage botting

A suggestion to avoid stupid bots to vote on polls was a token that gets
checked to a session cookie on vote submission.
This commit is contained in:
Wolvan 2022-01-12 19:46:45 +01:00
parent 6a155f2eb4
commit ab151cb732
3 changed files with 22 additions and 0 deletions

View file

@ -36,6 +36,7 @@
<section class="notepad"> <section class="notepad">
<div class="notepad-border"></div> <div class="notepad-border"></div>
<form action="{{ BACKEND_BASE_PATH }}/_backend/vote-form/{{ POLL_ID }}" method="POST"> <form action="{{ BACKEND_BASE_PATH }}/_backend/vote-form/{{ POLL_ID }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ CSRF_TOKEN }}">
<section class="poll-title"> <section class="poll-title">
{{ POLL_TITLE }} {{ POLL_TITLE }}
</section> </section>

View file

@ -4,6 +4,7 @@ import { CookieOptions, Router } from "express";
import { BackendPoll as Poll, DupeCheckMode } from "./Poll"; import { BackendPoll as Poll, DupeCheckMode } from "./Poll";
import { MAX_POLL_OPTIONS, MAX_CHARACTER_LENGTH } from "./Config"; import { MAX_POLL_OPTIONS, MAX_CHARACTER_LENGTH } from "./Config";
import Storage from "./Storage"; import Storage from "./Storage";
import crypto from "crypto";
function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") { function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") {
let result = ""; let result = "";
@ -213,12 +214,25 @@ export default async function init(router: Router, polls: Storage): Promise<void
const id = req.params.id; const id = req.params.id;
const votes = [].concat(req.body["poll-option"]); const votes = [].concat(req.body["poll-option"]);
const csrfTokenFromForm = req.body["csrf_token"];
const csrfTokenFromCookie = req.cookies.csrftoken;
if (csrfTokenFromForm !== csrfTokenFromCookie) return res.redirect(`/${id}?error=${
encodeURIComponent("Invalid CSRF token")
}&options=${
encodeURIComponent(votes.slice(0, MAX_POLL_OPTIONS).join("\uFFFE"))
}`);
const error = await voteOnPoll(id, votes, { const error = await voteOnPoll(id, votes, {
ip: req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "", ip: req.headers["x-forwarded-for"] as string || req.socket.remoteAddress || "",
setCookie: res.cookie.bind(res), setCookie: res.cookie.bind(res),
cookies: req.cookies cookies: req.cookies
}); });
res.cookie("csrftoken", crypto.randomBytes(32).toString("base64"), {
httpOnly: true,
});
if (!error) return res.redirect("/" + id + "/r"); if (!error) return res.redirect("/" + id + "/r");
if (error.statusCode === 404) return res.redirect("/"); if (error.statusCode === 404) return res.redirect("/");
res.redirect(`/${id}?error=${ res.redirect(`/${id}?error=${

View file

@ -7,6 +7,7 @@ import fetch from 'node-fetch';
import { program } from "commander"; import { program } from "commander";
import { FrontendPoll as Poll, PollResult } from "./Poll"; import { FrontendPoll as Poll, PollResult } from "./Poll";
import { MAX_CHARACTER_LENGTH, MAX_POLL_OPTIONS } from "./Config"; import { MAX_CHARACTER_LENGTH, MAX_POLL_OPTIONS } from "./Config";
import crypto from "crypto";
const RenderBuffer = new WeakMap(); const RenderBuffer = new WeakMap();
const RenderReplacements = new WeakMap(); const RenderReplacements = new WeakMap();
@ -158,7 +159,13 @@ export default function init(router: Router): void {
</div>` </div>`
).join(""); ).join("");
const csrfToken = req.cookies.csrftoken || crypto.randomBytes(32).toString("base64");
res.cookie("csrftoken", csrfToken, {
httpOnly: true,
});
await displayPage(req, res, "poll.html", { await displayPage(req, res, "poll.html", {
"CSRF_TOKEN": csrfToken,
"POLL_ID": poll.id, "POLL_ID": poll.id,
"POLL_TITLE": xss(poll.title), "POLL_TITLE": xss(poll.title),
"POLL_OPTION_DIVS": pollOptions, "POLL_OPTION_DIVS": pollOptions,