proper permissions system ready for board creation and assigning staff, and post password hashing

merge-requests/208/head
fatchan 5 years ago
parent 38b8c1f7e2
commit e7b4a60e95
  1. 1
      configs/main.json.example
  2. 20
      controllers/forms.js
  3. 4
      controllers/pages.js
  4. 8
      db/accounts.js
  5. 2
      gulpfile.js
  6. 40
      helpers/checks/actionchecker.js
  7. 2
      helpers/checks/bancheck.js
  8. 26
      helpers/checks/hasperms.js
  9. 20
      helpers/checks/haspermsmiddleware.js
  10. 2
      helpers/checks/spamcheck.js
  11. 31
      models/forms/actionhandler.js
  12. 20
      models/forms/makepost.js
  13. 2
      models/forms/register.js

@ -4,6 +4,7 @@
"sessionSecret": "long random string",
"tripcodeSecret": "long random string",
"ipHashSecret": "long random string",
"postPasswordSecret": "long random string",
"cacheTemplates": true,
"refererCheck": false,
"refererRegex": "^https?:\\/\\/(?:www\\.)?domain\\.com\\/",

@ -266,7 +266,7 @@ router.post('/board/:board/post', Boards.exists, banCheck, postFiles, paramConve
});
//board settings
router.post('/board/:board/settings', csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
router.post('/board/:board/settings', csrf, Boards.exists, checkPermsMiddleware(2), paramConverter, async (req, res, next) => {
const errors = [];
@ -321,7 +321,7 @@ router.post('/board/:board/settings', csrf, Boards.exists, checkPermsMiddleware,
});
//upload banners
router.post('/board/:board/addbanners', bannerFiles, csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
router.post('/board/:board/addbanners', bannerFiles, csrf, Boards.exists, checkPermsMiddleware(2), paramConverter, async (req, res, next) => {
if (req.files && req.files.file) {
if (Array.isArray(req.files.file)) {
@ -360,7 +360,7 @@ router.post('/board/:board/addbanners', bannerFiles, csrf, Boards.exists, checkP
});
//delete banners
router.post('/board/:board/deletebanners', csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
router.post('/board/:board/deletebanners', csrf, Boards.exists, checkPermsMiddleware(2), paramConverter, async (req, res, next) => {
const errors = [];
@ -397,7 +397,7 @@ router.post('/board/:board/deletebanners', csrf, Boards.exists, checkPermsMiddle
//actions for a specific board
router.post('/board/:board/actions', Boards.exists, banCheck, paramConverter, verifyCaptcha, boardActionController); //Captcha on regular actions
router.post('/board/:board/modactions', csrf, Boards.exists, checkPermsMiddleware, paramConverter, boardActionController); //CSRF for mod actions
router.post('/board/:board/modactions', csrf, Boards.exists, checkPermsMiddleware(3), paramConverter, boardActionController); //CSRF for mod actions
async function boardActionController(req, res, next) {
const errors = [];
@ -415,9 +415,9 @@ async function boardActionController(req, res, next) {
}
//check if they have permission to perform the actions
res.locals.hasPerms = checkPerms(req, res);
if(!res.locals.hasPerms) {
if (res.locals.actions.anyAuthed) {
res.locals.authLevel = checkPerms(req, res);
if (res.locals.authLevel >= 4) {
if (res.locals.authLevel > res.locals.actions.authRequired) {
errors.push('No permission');
}
if (req.body.delete && !res.locals.board.settings.userPostDelete) {
@ -472,7 +472,7 @@ async function boardActionController(req, res, next) {
}
//global actions (global manage page)
router.post('/global/actions', csrf, checkPermsMiddleware, paramConverter, globalActionController);
router.post('/global/actions', csrf, checkPermsMiddleware(1), paramConverter, globalActionController);
async function globalActionController(req, res, next) {
const errors = [];
@ -526,7 +526,7 @@ async function globalActionController(req, res, next) {
}
//unban
router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware, paramConverter, async (req, res, next) => {
router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware(3), paramConverter, async (req, res, next) => {
//keep this for later in case i add other options to unbans
const errors = [];
@ -558,7 +558,7 @@ router.post('/board/:board/unban', csrf, Boards.exists, checkPermsMiddleware, pa
});
router.post('/global/unban', csrf, checkPermsMiddleware, paramConverter, async(req, res, next) => {
router.post('/global/unban', csrf, checkPermsMiddleware(1), paramConverter, async(req, res, next) => {
const errors = [];

@ -57,10 +57,10 @@ router.get('/randombanner', randombanner);
router.get('/:board/banners.html', Boards.exists, banners);
//board manage page
router.get('/:board/manage.html', Boards.exists, isLoggedIn, hasPerms, csrf, manage);
router.get('/:board/manage.html', Boards.exists, isLoggedIn, hasPerms(3), csrf, manage);
//global manage page
router.get('/globalmanage.html', isLoggedIn, hasPerms, csrf, globalManage);
router.get('/globalmanage.html', isLoggedIn, hasPerms(1), csrf, globalManage);
// board page/recents
router.get('/:board/:page(1[0-9]*|[2-9]*|index).html', Boards.exists, paramConverter, board);

@ -12,14 +12,6 @@ module.exports = {
},
insertOne: async (username, password, authLevel) => {
/* auth levels
3: site admin/owner -- all permissions e.g. post/board/board config management
2: global mod -- delete posts anywhere
1: regular user -- permissions for boards they own or were given moderator on
on user-created boards (planned feature), only owner can delete board or change board settings
assigned moderators can delete posts.
*/
// hash the password
const passwordHash = await bcrypt.hash(password, 12);

@ -181,7 +181,7 @@ async function wipe() {
});
//default admin acc
await Accounts.insertOne('admin', 'changeme', 3);
await Accounts.insertOne('admin', 'changeme', 0);
Mongo.client.close();
//delete all the static files

@ -1,28 +1,28 @@
'use strict';
const actions = [
{name:'unlink_file', global:true, auth:false, passwords:true, build:true},
{name:'delete_file', global:true, auth:true, passwords:false, build:true},
{name:'spoiler', global:true, auth:false, passwords:true, build:true},
{name:'delete', global:true, auth:false, passwords:true, build:true},
{name:'lock', global:false, auth:true, passwords:false, build:true},
{name:'sticky', global:false, auth:true, passwords:false, build:true},
{name:'cyclic', global:false, auth:true, passwords:false, build:true},
{name:'sage', global:false, auth:true, passwords:false, build:true},
{name:'report', global:false, auth:false, passwords:false, build:false},
{name:'global_report', global:false, auth:false, passwords:false, build:false},
{name:'delete_ip_board', global:false, auth:true, passwords:false, build:true},
{name:'delete_ip_global', global:true, auth:true, passwords:false, build:true},
{name:'dismiss', global:false, auth:true, passwords:false, build:false},
{name:'global_dismiss', global:true, auth:true, passwords:false, build:false},
{name:'ban', global:false, auth:true, passwords:false, build:true},
{name:'global_ban', global:true, auth:true, passwords:false, build:true},
{name:'unlink_file', global:true, auth:3, passwords:true, build:true},
{name:'delete_file', global:true, auth:1, passwords:false, build:true},
{name:'spoiler', global:true, auth:4, passwords:true, build:true},
{name:'delete', global:true, auth:4, passwords:true, build:true},
{name:'lock', global:false, auth:3, passwords:false, build:true},
{name:'sticky', global:false, auth:3, passwords:false, build:true},
{name:'cyclic', global:false, auth:3, passwords:false, build:true},
{name:'sage', global:false, auth:3, passwords:false, build:true},
{name:'report', global:false, auth:4, passwords:false, build:false},
{name:'global_report', global:true, auth:4, passwords:false, build:false},
{name:'delete_ip_board', global:true, auth:3, passwords:false, build:true},
{name:'delete_ip_global', global:true, auth:1, passwords:false, build:true},
{name:'dismiss', global:false, auth:3, passwords:false, build:false},
{name:'global_dismiss', global:true, auth:1, passwords:false, build:false},
{name:'ban', global:false, auth:3, passwords:false, build:true},
{name:'global_ban', global:true, auth:1, passwords:false, build:true},
];
module.exports = (req, res) => {
let anyGlobal = 0
, anyAuthed = 0
, authRequired = 4
, anyPasswords = 0
, anyBuild = 0
, anyValid = 0;
@ -35,8 +35,8 @@ module.exports = (req, res) => {
if (action.global) {
anyGlobal++;
}
if (action.auth) {
anyAuthed++;
if (action.auth && action.auth < authRequired) {
authRequired = action.auth;
}
if (action.passwords) {
anyPasswords++;
@ -47,6 +47,6 @@ module.exports = (req, res) => {
}
}
return { anyGlobal, anyAuthed, anyValid, anyPasswords, anyBuild };
return { anyGlobal, authRequired, anyValid, anyPasswords, anyBuild };
}

@ -5,7 +5,7 @@ const Bans = require(__dirname+'/../../db/bans.js')
module.exports = async (req, res, next) => {
if (!hasPerms(req, res)) {
if (hasPerms(req, res) <= 1) {
const bans = await Bans.find(res.locals.ip, res.locals.board ? res.locals.board._id : null);
if (bans && bans.length > 0) {
//TODO: show posts banned for, expiry, etc

@ -1,16 +1,18 @@
'use strict';
module.exports = (req, res) => {
return req.session.authenticated //if the user is authed
&& req.session.user //if the user is logged in
&& (
req.session.user.authLevel > 1 //and is not a regular user
|| (
res.locals.board
&& (
res.locals.board.owner == req.session.user.username //and board owner
|| res.locals.board.moderators.includes(req.session.user.username) //or board mod
)
)
)
const { authenticated, user } = req.session;
if (authenticated === true && user != null) {
if (user.authLevel <= 1) {
return user.authLevel; //admin 0, global staff, 1
}
if (res.locals.board != null) {
if (res.locals.board.owner === user.username) {
return 2; //board owner 2
} else if (res.locals.board.moderators.includes(user.username) === true) {
return 3; //board staff 3
}
}
}
return 4; //not logged in/too low level for anything atm
}

@ -2,16 +2,18 @@
const hasPerms = require(__dirname+'/hasperms.js');
module.exports = async (req, res, next) => {
module.exports = (requiredLevel) => {
res.locals.hasPerms = hasPerms(req, res);
if (!res.locals.hasPerms) {
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'You do not have permission to access this page',
'redirect': '/'
});
return function(req, res, next) {
const authLevel = hasPerms(req, res);
if (authLevel > requiredLevel) {
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'No Permission',
'redirect': '/'
});
}
next();
}
next();
}

@ -7,7 +7,7 @@ const Mongo = require(__dirname+'/../../db/db.js')
module.exports = async (req, res) => {
if (hasPerms(req, res)) {
if (hasPerms(req, res) <= 1) { //global staff bypass spam check
return false;
}

@ -18,7 +18,8 @@ const Posts = require(__dirname+'/../../db/posts.js')
, { remove } = require('fs-extra')
, uploadDirectory = require(__dirname+'/../../helpers/files/uploadDirectory.js')
, { buildCatalog, buildThread, buildBoardMultiple } = require(__dirname+'/../../helpers/build.js')
, { timingSafeEqual } = require('crypto');
, { postPasswordSecret } = require(__dirname+'/../../configs/main.json')
, { createHash, timingSafeEqual } = require('crypto');
module.exports = async (req, res, next) => {
@ -26,18 +27,22 @@ module.exports = async (req, res, next) => {
const postMongoIds = res.locals.posts.map(post => Mongo.ObjectId(post._id));
let passwordPostMongoIds = [];
let passwordPosts = [];
if (!res.locals.hasPerms && res.locals.actions.anyPasswords) {
//just to avoid multiple filters and mapping, do it all here
const inputBuffer = Buffer.from(req.body.password || '', 0, 100);
passwordPosts = res.locals.posts.filter(post => {
const postBuffer = Buffer.from(post.password || '', 0, 100);
if (timingSafeEqual(inputBuffer, postBuffer) === true
&& post.password != null
&& post.password.length > 0) {
passwordPostMongoIds.push(Mongo.ObjectId(post._id))
return true;
}
});
if (res.locals.authLevel >= 4 && res.locals.actions.anyPasswords) {
if (req.body.password && req.body.password.length > 0) {
//hash their input and make it a buffer
const inputPasswordHash = createHash('sha256').update(postPasswordSecret + req.body.password).digest('base64');
const inputPasswordBuffer = Buffer.from(inputPasswordHash);
passwordPosts = res.locals.posts.filter(post => {
//length comparison could reveal the length, but not contents, and is better than comparing and hashing for empty password (most posts)
if (post.password != null && post.password.length === req.body.password) {
const postBuffer = Buffer.from(post.password);
if (timingSafeEqual(inputBuffer, postBuffer) === true) {
passwordPostMongoIds.push(Mongo.ObjectId(post._id));
return true;
}
}
});
}
if (passwordPosts.length === 0) {
return res.status(403).render('message', {
'title': 'Forbidden',

@ -31,6 +31,7 @@ const path = require('path')
, deleteTempFiles = require(__dirname+'/../../helpers/files/deletetempfiles.js')
, msTime = require(__dirname+'/../../helpers/mstime.js')
, deletePosts = require(__dirname+'/deletepost.js')
, { postPasswordSecret } = require(__dirname+'/../../configs/main.json')
, { buildCatalog, buildThread, buildBoard, buildBoardMultiple } = require(__dirname+'/../../helpers/build.js');
module.exports = async (req, res, next) => {
@ -39,7 +40,7 @@ module.exports = async (req, res, next) => {
let redirect = `/${req.params.board}/`
let salt = null;
let thread = null;
const hasPerms = permsCheck(req, res);
const permLevel = permsCheck(req, res);
const { filters, maxFiles, forceAnon, replyLimit, threadLimit, ids, userPostSpoiler, defaultName, captchaTrigger, captchaTriggerMode, captchaMode } = res.locals.board.settings;
if (req.body.thread) {
thread = await Posts.getPost(req.params.board, req.body.thread, true);
@ -53,7 +54,7 @@ module.exports = async (req, res, next) => {
}
salt = thread.salt;
redirect += `thread/${req.body.thread}.html`
if (thread.locked && !hasPerms) {
if (thread.locked && permLevel >= 4) {
await deleteTempFiles(req).catch(e => console.error);
return res.status(400).render('message', {
'title': 'Bad request',
@ -200,10 +201,15 @@ module.exports = async (req, res, next) => {
userId = fullUserIdHash.substring(fullUserIdHash.length-6);
}
let password = null;
if (req.body.password) {
password = createHash('sha256').update(postPasswordSecret + req.body.password).digest('base64');
}
//forceanon hide reply subjects so cant be used as name for replies
//forceanon only allow sage email
let subject = (hasPerms || !forceAnon || !req.body.thread) ? req.body.subject : null;
let email = (hasPerms || !forceAnon || req.body.email === 'sage') ? req.body.email : null;
let subject = (permLevel < 4 || !forceAnon || !req.body.thread) ? req.body.subject : null;
let email = (permLevel < 4 || !forceAnon || req.body.email === 'sage') ? req.body.email : null;
//spoiler files only if board settings allow
const spoiler = userPostSpoiler && req.body.spoiler ? true : false;
@ -211,7 +217,7 @@ module.exports = async (req, res, next) => {
let name = defaultName;
let tripcode = null;
let capcode = null;
if ((hasPerms || !forceAnon) && req.body.name && req.body.name.length > 0) {
if ((permLevel < 4 || !forceAnon) && req.body.name && req.body.name.length > 0) {
// get matches with named groups for name, trip and capcode in 1 regex
const matches = req.body.name.match(nameRegex);
if (matches && matches.groups) {
@ -225,7 +231,7 @@ module.exports = async (req, res, next) => {
tripcode = `!!${(await getTripCode(groups.tripcode))}`;
}
//capcode
if (groups.capcode && hasPerms) {
if (groups.capcode && permLevel < 4) {
// TODO: add proper code for different capcodes
capcode = `## ${groups.capcode}`;
}
@ -254,7 +260,7 @@ module.exports = async (req, res, next) => {
'message': message || null,
'nomarkup': req.body.message || null,
'thread': req.body.thread || null,
'password': req.body.password || null,
password,
email,
spoiler,
'banmessage': null,

@ -26,7 +26,7 @@ module.exports = async (req, res, next) => {
// add account to db. password is hashed in db model func for easier tests
try {
await Accounts.insertOne(username, password, 1);
await Accounts.insertOne(username, password, 4);
} catch (err) {
return next(err);
}

Loading…
Cancel
Save