add captcha, csrf and sessions
This commit is contained in:
parent
e8ca1fcaed
commit
d46eeacf41
|
@ -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,6 +1,7 @@
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# code
|
# code
|
||||||
!*.js
|
!*.js
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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;
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue