add captcha, csrf and sessions

This commit is contained in:
b1ek 2023-04-17 10:37:07 +10:00
parent e8ca1fcaed
commit d46eeacf41
Signed by: blek
GPG Key ID: 14546221E3595D0C
11 changed files with 163 additions and 4 deletions

View File

@ -5,4 +5,7 @@ MAXLEN=5120
MAXFILES=128 MAXFILES=128
SHOW_SUBMITTED=true SHOW_SUBMITTED=true
ADMIN_EMAIL=john.doe@example.com ADMIN_EMAIL=john.doe@example.com
SESSION_SECRET=
SESSION_MEMCACHE_HOST=memcache:11211

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
node_modules node_modules
.env .env
package-lock.json package-lock.json
yarn.lock
# code # code
!*.js !*.js

View File

@ -10,5 +10,17 @@ services:
volumes: volumes:
- './usercontent:/opt/code/usercontent' - './usercontent:/opt/code/usercontent'
# uncomment this for debug mode # uncomment this for debug mode
#- './:/opt/code' - './:/opt/code'
restart: always restart: always
networks:
- bin
memcache:
image: memcached
restart: always
networks:
- bin
networks:
bin:
driver: bridge

17
gen_key.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
errcho() {
echo $* >&2
}
if [[ $1 != '-y' ]]; then
errcho -e "\033[31mERROR: \033[0mThis will overwrite your current key."
errcho -e "\033[31mERROR: \033[0mRun it again with -y as first argument to confirm."
exit 1
fi
KEY=$(cat /dev/urandom | tr -dc '[:alpha:]' | fold -w 32 | head -n 1)
sed -i "s/^SESSION_SECRET=[a-zA-Z0-9]*$/SESSION_SECRET=$KEY/g" .env
echo Your key is $KEY

View File

@ -5,4 +5,22 @@ const bodyparse = require('body-parser');
router.use(bodyparse.json()); router.use(bodyparse.json());
router.use(bodyparse.urlencoded({extended: true})); router.use(bodyparse.urlencoded({extended: true}));
const session = require('express-session');
const memcache = require("connect-memcached")(session);
const crypto = require('crypto');
router.use(
session({
secret: process.env.SESSION_SECRET,
secure: true,
resave: false,
saveUninitialized: true,
store: new memcache({
hosts: [process.env.SESSION_MEMCACHE_HOST],
secret: process.env.SESSION_SECRET +
crypto.createHash('sha256', process.env.SESSION_SECRET).digest().toString('hex')
})
})
);
module.exports = router; module.exports = router;

View File

@ -12,9 +12,11 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"connect-memcached": "^2.0.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-session": "^1.17.3",
"fancy-log": "^2.0.0", "fancy-log": "^2.0.0",
"glob": "^9.2.1", "glob": "^9.2.1",
"pug": "^3.0.2" "pug": "^3.0.2"

View File

@ -46,4 +46,19 @@ input[type=submit] {
} }
input[type=submit]:hover { input[type=submit]:hover {
box-shadow: 0 2px 4px #60806040; box-shadow: 0 2px 4px #60806040;
}
div.captcha_box {
margin:0 auto;
width:100px;
text-align:center;
background-color: white;
padding: 20px;
border: 1px solid gray;
}
p.c_s {
margin:0;
padding: 0;
font-size:11pt;
font-family: sans-serif
} }

View File

@ -3,11 +3,25 @@ const router = express.Router();
const handler = require('express-async-handler'); const handler = require('express-async-handler');
const content = require('../helpers/content'); const content = require('../helpers/content');
const crypto = require('crypto');
async function index(req, res) { async function index(req, res) {
if (!req.session.captcha) {
req.session.captcha = crypto.randomBytes(8).toString('base64').substring(0, 6);
}
req.session.captcha_input = crypto.randomBytes(8).toString('base64').substring(0,10);
if (!req.session.csrf)
req.session.csrf = crypto.randomBytes(10).toString('base64');
res.render('main', { res.render('main', {
maxlen: process.env.MAXLEN, maxlen: process.env.MAXLEN,
submitted: content.submitted() submitted: content.submitted(),
req,
crypto
}); });
} }
router.get('/', handler(index)); router.get('/', handler(index));

View File

@ -2,11 +2,30 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const handler = require('express-async-handler'); const handler = require('express-async-handler');
const content = require('../helpers/content'); const content = require('../helpers/content');
const crypto = require('crypto');
const { MAXFILES } = process.env; const { MAXFILES } = process.env;
async function upload(req, res) { async function upload(req, res) {
if (req.body['_csrf'] != req.session.csrf) {
res.status(405).send('CSRF error');
return;
}
if (!req.body[req.session.captcha_input]) {
res.status(405).send('Captcha error; please go back and refresh the page a few times.');
return;
}
if (req.body[req.session.captcha_input] != req.session.captcha) {
res.status(405).send('Bad captcha');
return;
}
req.session.captcha = crypto.randomBytes(8).toString('base64').substring(0,6);
if (content.submitted() >= MAXFILES) { if (content.submitted() >= MAXFILES) {
res.status(405).send('Not allowed'); res.status(405).send('Not allowed');
return; return;

46
views/captcha.pug Normal file
View File

@ -0,0 +1,46 @@
mixin captcha(text)
div(class='captcha_box')
-
const pyRange = (start, stop, step) =>
Array.from(
{ length: (stop - start) / step + 1 },
(value, index) => start + index * step
);
var captcha = text
var shuffled_indexes = pyRange(0, captcha.length - 1, 1);
shuffled_indexes.sort(() => crypto.randomInt(0,10) > 5 ? 1 : -1);
var current_index = -1;
const rint = (m, mx) => crypto.randomInt(m, mx);
const blowfish = () => {
const rules = [
'font-weight:normal',
'font-family:inherit',
'font-weight:bold',
'font-weight:normal',
'display:block',
'display:inline-block',
'display:flex',
'display:none',
'display:none'
];
const n = rint(8, 16);
let out = '';
for (let i = 0; i < n; i++) {
out += rules[rint(0,rules.length - 1)] + ';'.repeat(rint(1,6))
}
return out;
}
each index in shuffled_indexes
-
current_index++
var left_margin = 0;
p(class='c_s' style=`transform:translate(${((index - current_index) * (rint(80,82) / 10))}px, ${rint(-3,3)}px);${blowfish()};font-weight:${rint(0,2) == 1 ? 'bold' : 'normal'};color:#220000;display:inline-block`)= captcha[index]
each i in pyRange(0, rint(4,10), 1)
p(class='c_s' style=`transform:translate(${((index - current_index) * (rint(80,82) / 10))}px, ${rint(-3,3)}px);${blowfish()};font-weight:bold;color:#220000;display:none`)= a

View File

@ -1,13 +1,25 @@
extends template/main.pug extends template/main.pug
block root
include captcha.pug
block content block content
- var exceeded = submitted >= process.env.MAXFILES - var exceeded = submitted >= process.env.MAXFILES
form(action='/upload' method='POST') form(action='/upload' method='POST')
input(type='hidden' name='_csrf' value=req.session.csrf)
p(align='center') p(align='center')
textarea(name='text' class='data' placeholder='Put your text in here!' + (maxlen ? ` (Max length is ${maxlen} bytes)` : '')) textarea(name='text' class='data' placeholder='Put your text in here!' + (maxlen ? ` (Max length is ${maxlen} bytes)` : ''))
br br
if (!exceeded) if (!exceeded)
br
| Captcha:
br
input(type='text' name=req.session.captcha_input)
if (!exceeded)
+captcha(req.session.captcha)
p(align='center' style='padding-bottom:10px')
input(type='submit' value='Upload!') input(type='submit' value='Upload!')
if (exceeded) if (exceeded)
p(style='color:darkred;font-weight:bold;font-size:9pt' align='center') p(style='color:darkred;font-weight:bold;font-size:9pt' align='center')
| Max uploads limit exceeded. No more uploads would be accepted. | Max uploads limit exceeded. No more uploads would be accepted.