Merge branch '435-account-permission-bypass-captcha' into develop

indiachan-spamvector
Thomas Lynch 2 years ago
commit 8752b12713
  1. 27
      controllers/forms.js
  2. 4
      controllers/pages.js
  3. 17
      db/bypass.js
  4. 2
      lib/captcha/captcha.js
  5. 32
      lib/middleware/captcha/blockbypass.js
  6. 83
      lib/middleware/captcha/torprebypass.js
  7. 10
      lib/middleware/captcha/verify.js
  8. 9
      lib/middleware/file/filemiddlewares.js
  9. 8
      lib/middleware/ip/processip.js
  10. 1
      lib/permission/permissions.js
  11. 1
      lib/permission/permissiontext.js
  12. 2
      models/forms/blockbypass.js
  13. 1
      models/forms/editaccount.js

@ -4,8 +4,7 @@ const express = require('express')
, router = express.Router({ caseSensitive: true })
, Boards = require(__dirname+'/../db/boards.js')
//middlewares
, torPreBypassCheck = require(__dirname+'/../lib/middleware/captcha/torprebypass.js')
, geoAndTor = require(__dirname+'/../lib/middleware/ip/geoip.js')
, geoIp = require(__dirname+'/../lib/middleware/ip/geoip.js')
, processIp = require(__dirname+'/../lib/middleware/ip/processip.js')
, calcPerms = require(__dirname+'/../lib/middleware/permission/calcpermsmiddleware.js')
, Permissions = require(__dirname+'/../lib/permission/permissions.js')
@ -32,29 +31,29 @@ const express = require('express')
editRoleController, newCaptchaForm, blockBypassForm, logoutForm, deleteSessionsController } = require(__dirname+'/forms/index.js');
//make new post
router.post('/board/:board/post', geoAndTor, fileMiddlewares.postsEarly, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, fileMiddlewares.posts,
router.post('/board/:board/post', geoIp, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, fileMiddlewares.posts,
makePostController.paramConverter, verifyCaptcha, numFiles, blockBypass.middleware, dnsblCheck, imageHashes, makePostController.controller);
router.post('/board/:board/modpost', geoAndTor, fileMiddlewares.postsEarly, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn,
router.post('/board/:board/modpost', geoIp, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), fileMiddlewares.posts, makePostController.paramConverter, csrf, numFiles, blockBypass.middleware, dnsblCheck, imageHashes, makePostController.controller); //mod post has token instead of captcha
//post actions
router.post('/board/:board/actions', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, actionController.paramConverter, verifyCaptcha, actionController.controller); //public, with captcha
router.post('/board/:board/modactions', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn,
router.post('/board/:board/actions', geoIp, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, actionController.paramConverter, verifyCaptcha, actionController.controller); //public, with captcha
router.post('/board/:board/modactions', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), actionController.paramConverter, actionController.controller); //board manage page
router.post('/global/actions', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
router.post('/global/actions', geoIp, processIp, useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_GLOBAL_GENERAL), globalActionController.paramConverter, globalActionController.controller); //global manage page
//appeal ban
router.post('/appeal', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, appealController.paramConverter, verifyCaptcha, appealController.controller);
router.post('/appeal', geoIp, processIp, useSession, sessionRefresh, appealController.paramConverter, verifyCaptcha, appealController.controller);
//edit post
router.post('/editpost', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, csrf, editPostController.paramConverter, Boards.bodyExists, calcPerms,
router.post('/editpost', geoIp, processIp, useSession, sessionRefresh, csrf, editPostController.paramConverter, Boards.bodyExists, calcPerms,
hasPerms.any(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL), editPostController.controller);
//board management forms
router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), transferController.paramConverter, transferController.controller);
router.post('/board/:board/settings', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/settings', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), boardSettingsController.paramConverter, boardSettingsController.controller);
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_BANS), editBansController.paramConverter, editBansController.controller); //edit bans
@ -108,13 +107,13 @@ router.post('/global/settings', useSession, sessionRefresh, csrf, calcPerms, isL
hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), globalSettingsController.paramConverter, globalSettingsController.controller); //global settings
//create board
router.post('/create', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, isLoggedIn, verifyCaptcha, calcPerms, createBoardController.paramConverter, createBoardController.controller);
router.post('/create', geoIp, processIp, useSession, sessionRefresh, isLoggedIn, calcPerms, verifyCaptcha, createBoardController.paramConverter, createBoardController.controller);
//accounts
router.post('/login', useSession, loginController.paramConverter, loginController.controller);
router.post('/logout', useSession, logoutForm);
router.post('/register', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, verifyCaptcha, calcPerms, registerController.paramConverter, registerController.controller);
router.post('/changepassword', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, verifyCaptcha, changePasswordController.paramConverter, changePasswordController.controller);
router.post('/register', geoIp, processIp, useSession, sessionRefresh, calcPerms, verifyCaptcha, registerController.paramConverter, registerController.controller);
router.post('/changepassword', geoIp, processIp, useSession, sessionRefresh, verifyCaptcha, changePasswordController.paramConverter, changePasswordController.controller);
router.post('/resign', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, resignController.paramConverter, resignController.controller);
router.post('/deleteaccount', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteAccountController.controller);
router.post('/deletesessions', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteSessionsController.paramConverter, deleteSessionsController.controller);
@ -122,7 +121,7 @@ router.post('/deletesessions', useSession, sessionRefresh, csrf, calcPerms, isLo
//removes captcha cookie, for refreshing for noscript users
router.post('/newcaptcha', newCaptchaForm);
//solve captcha for block bypass
router.post('/blockbypass', geoAndTor, processIp, verifyCaptcha, blockBypassForm);
router.post('/blockbypass', geoIp, processIp, useSession, sessionRefresh, calcPerms, verifyCaptcha, blockBypassForm);
module.exports = router;

@ -6,7 +6,7 @@ const express = require('express')
, Posts = require(__dirname+'/../db/posts.js')
//middlewares
, processIp = require(__dirname+'/../lib/middleware/ip/processip.js')
, geoAndTor = require(__dirname+'/../lib/middleware/ip/geoip.js')
, geoIp = require(__dirname+'/../lib/middleware/ip/geoip.js')
, calcPerms = require(__dirname+'/../lib/middleware/permission/calcpermsmiddleware.js')
, Permissions = require(__dirname+'/../lib/permission/permissions.js')
, hasPerms = require(__dirname+'/../lib/middleware/permission/haspermsmiddleware.js')
@ -111,7 +111,7 @@ router.get('/globalmanage/editrole/:roleid([a-f0-9]{24}).html', useSession, sess
//TODO: edit post edit page form, like editnews/editaccount/editrole endpoint
//captcha
router.get('/captcha', geoAndTor, processIp, captcha); //get captcha image and cookie
router.get('/captcha', geoIp, processIp, captcha); //get captcha image and cookie
router.get('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page
router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page

@ -9,27 +9,30 @@ module.exports = {
db,
checkBypass: (id, anonymizer=false) => {
const { blockBypass } = config.get;
return db.findOneAndUpdate({
'_id': id,
'anonymizer': anonymizer,
'uses': {
'$lte': blockBypass.expireAfterUses
'$gt': 0
}
}, {
'$inc': {
'uses': 1,
'uses': -1,
}
}).then(r => r.value);
},
getBypass: (anonymizer=false) => {
getBypass: (anonymizer=false, id=null, uses=0) => {
const { blockBypass } = config.get;
return db.insertOne({
'uses': 0,
const newBypass = {
'uses': uses,
'anonymizer': anonymizer,
'expireAt': new Date(Date.now() + blockBypass.expireAfterTime)
});
};
if (anonymizer === true && id !== null) {
newBypass._id = Mongo.ObjectId(id);
}
return db.insertOne(newBypass);
},
deleteAll: () => {

@ -2,7 +2,7 @@
const { Captchas } = require(__dirname+'/../../db/')
, { ObjectId } = require(__dirname+'/../../db/db.js')
, config = require(__dirname+'/..//misc/config.js')
, config = require(__dirname+'/../misc/config.js')
, { hcaptcha, google } = require(__dirname+'/../../configs/secrets.js')
, FormData = require('form-data')
, fetch = require('node-fetch')

@ -10,6 +10,7 @@ const { Bypass } = require(__dirname+'/../../../db/')
module.exports = {
check: async (req, res, next) => {
const { secureCookies, blockBypass } = config.get;
//check if blockbypass exists and right length
@ -27,28 +28,26 @@ module.exports = {
});
}
//try to get bypass from db and make sure uses < maxUses
//try to get bypass from db
let bypass;
if (bypassId && bypassId.length === 24) {
try {
const bypassMongoId = ObjectId(bypassId);
bypass = await Bypass.checkBypass(bypassMongoId, res.locals.anonymizer);
res.locals.blockBypass = true;
} catch (err) {
return next(err);
}
}
if (bypass //if they have a valid bypass
&& (bypass.uses < blockBypass.expireAfterUses //and its not overused
|| (res.locals.anonymizer
&& !blockBypass.forceAnonymizers))) { //OR its not forced for anonymizers
//next if they have a valid bypass
if (bypass != null) {
res.locals.blockBypass = true;
return next();
}
if (res.locals.solvedCaptcha) {
//they dont have a valid bypass, but just solved board captcha, so give them a new one
const newBypass = await Bypass.getBypass(res.locals.anonymizer);
const newBypass = await Bypass.getBypass(res.locals.anonymizer, res.locals.pseudoIp, blockBypass.expireAfterUses);
const newBypassId = newBypass.insertedId;
res.locals.blockBypass = true;
res.cookie('bypassid', newBypassId.toString(), {
@ -61,6 +60,7 @@ module.exports = {
}
deleteTempFiles(req).catch(console.error);
res.clearCookie('bypassid');
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Block bypass expired or exceeded max uses',
@ -74,11 +74,19 @@ module.exports = {
},
middleware: async (req, res, next) => {
const { blockBypass } = config.get;
if (res.locals.preFetchedBypassId //if they already have a bypass
|| (!blockBypass.enabled //or if block bypass isnt enabled
&& (!blockBypass.forceAnonymizers //and we dont force it for anonymizer
|| !res.locals.anonymizer))) { //or they arent on an anonymizer
const { blockBypass, secureCookies } = config.get;
//skip bypass check if not enabled, and not forced for anonymizer or we arent an anonymizer
if (!blockBypass.enabled &&
(!blockBypass.forceAnonymizers || !res.locals.anonymizer)) {
if (res.locals.anonymizer) {
//dummy for anonymizers, wont work once bypasses are enabled. just allows them to keep same ip/userId for session
res.cookie('bypassid', res.locals.pseudoIp, {
'maxAge': blockBypass.expireAfterTime,
'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
'sameSite': 'strict',
'signed': true
});
}
return next();
}
return module.exports.check(req, res, next);

@ -1,83 +0,0 @@
'use strict';
const { Bypass } = require(__dirname+'/../../../db/')
, config = require(__dirname+'/../../misc/config.js')
, checkCaptcha = require(__dirname+'/../../captcha/captcha.js')
, remove = require('fs-extra').remove
, uploadDirectory = require(__dirname+'/../../file/uploaddirectory.js')
, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
, deleteTempFiles = require(__dirname+'/../../file/deletetempfiles.js')
, production = process.env.NODE_ENV === 'production';
module.exports = async (req, res, next) => {
//early bypass is only needed for anonymizer users
if (!res.locals.anonymizer) {
return next();
}
let bypassId = req.signedCookies.bypassid;
const { secureCookies, blockBypass } = config.get;
if (blockBypass.enabled || blockBypass.forceAnonymizers) {
const input = req.body.captcha;
const captchaId = req.cookies.captchaid;
if (input && !bypassId) {
// try to get the captcha from the DB
try {
await checkCaptcha(input, captchaId);
} catch (err) {
deleteTempFiles(req).catch(console.error);
if (err instanceof Error) {
return next(err);
}
const page = (req.body.minimal || req.path === '/blockbypass' ? 'bypass' : 'message');
return dynamicResponse(req, res, 403, page, {
'title': 'Forbidden',
'message': err,
'redirect': req.headers.referer,
});
}
res.locals.solvedCaptcha = true;
res.clearCookie('captchaid');
remove(`${uploadDirectory}/captcha/${captchaId}.jpg`).catch(e => { console.error(e); });
}
}
if (res.locals.solvedCaptcha //if they just solved a captcha
|| (!blockBypass.enabled //OR blockbypass isnt enabled
&& !blockBypass.forceAnonymizers //AND its not forced for anonymizers
&& !bypassId)) { //AND they dont already have one,
//then give the user a bypass id
const newBypass = await Bypass.getBypass(res.locals.anonymizer);
const newBypassId = newBypass.insertedId;
bypassId = newBypassId.toString();
res.locals.preFetchedBypassId = bypassId;
res.locals.blockBypass = true;
res.cookie('bypassid', newBypassId.toString(), {
'maxAge': blockBypass.expireAfterTime,
'secure': production && secureCookies && (req.headers['x-forwarded-proto'] === 'https'),
'sameSite': 'strict',
'signed': true
});
return next();
}
//check if blockbypass exists and right length
if (!bypassId || bypassId.length !== 24) {
res.clearCookie('bypassid');
deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Please complete a block bypass to continue',
'frame': '/bypass_minimal.html',
'link': {
'href': '/bypass.html',
'text': 'Get block bypass',
},
});
}
return next();
};

@ -6,7 +6,8 @@ const { Ratelimits } = require(__dirname+'/../../../db/')
, remove = require('fs-extra').remove
, dynamicResponse = require(__dirname+'/../../misc/dynamic.js')
, deleteTempFiles = require(__dirname+'/../../file/deletetempfiles.js')
, uploadDirectory = require(__dirname+'/../../file/uploaddirectory.js');
, uploadDirectory = require(__dirname+'/../../file/uploaddirectory.js')
, Permissions = require(__dirname+'/../../permission/permissions.js');
module.exports = async (req, res, next) => {
@ -15,6 +16,13 @@ module.exports = async (req, res, next) => {
return next();
}
//bypass captcha permission
if (res.locals.permissions &&
res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) {
res.locals.solvedCaptcha = true;
return next();
}
//skip captcha if disabled on board for posts only
if (res.locals.board
&& req.path === `/board/${res.locals.board._id}/post`) {

@ -69,16 +69,7 @@ module.exports = {
return fileHandlers.flag(req, res, next);
},
posts: (req, res, next) => {
if (res.locals.anonymizer) {
return next();
}
return fileHandlers.post(req, res, next);
},
postsEarly: (req, res, next) => {
if (res.locals.anonymizer) {
return fileHandlers.post(req, res, next);
}
return next();
},
};

@ -2,13 +2,15 @@
const config = require(__dirname+'/../../misc/config.js')
, { createCIDR, parse } = require('ip6addr')
, hashIp = require(__dirname+'/../../misc/haship.js');
, hashIp = require(__dirname+'/../../misc/haship.js')
, { ObjectId } = require(__dirname+'/../../../db/db.js');
module.exports = (req, res, next) => {
//tor user ip uses bypass id, if they dont have one send to blockbypass
//tor user ip uses bypass id (or objectid, which will become bypassid)
if (res.locals.anonymizer) {
const pseudoIp = res.locals.preFetchedBypassId || req.signedCookies.bypassid;
const pseudoIp = req.signedCookies.bypassid || ObjectId().toString();
res.locals.pseudoIp = pseudoIp;
res.locals.ip = {
raw: `${pseudoIp}.BP`,
cloak: `${pseudoIp}.BP`,

@ -9,6 +9,7 @@ const Permissions = {
BYPASS_SPAMCHECK: 5,
BYPASS_RATELIMITS: 6,
BYPASS_FILTERS: 7,
BYPASS_CAPTCHA: 8,
MANAGE_GLOBAL_GENERAL: 10,
MANAGE_GLOBAL_BANS: 11,
MANAGE_GLOBAL_LOGS: 12,

@ -10,6 +10,7 @@ module.exports = {
BYPASS_SPAMCHECK: { label: 'Bypass Spamcheck', desc: 'Bypass the basic anti-flood spamcheck for too frequent similar posting.' },
BYPASS_RATELIMITS: { label: 'Bypass Ratelimits', desc: 'Bypass ratelimits for getting new captchas, editing posts, editing board settings, etc.' },
BYPASS_FILTERS: { label: 'Bypass Filters', desc: 'Bypass all post filters.' },
BYPASS_CAPTCHA: { label: 'Bypass Captcha', desc: 'Bypass captcha.' },
MANAGE_GLOBAL_GENERAL: { title: 'Global Management',label: 'Global Staff', desc: 'General global staff permission. Access to recent posts and reports. Ability to submit global actions.' },
MANAGE_GLOBAL_BANS: { label: 'Global Bans', desc: 'Access global bans. Ability to unban, edit, or deny appeals.' },
MANAGE_GLOBAL_LOGS: { label: 'Global Logs', desc: 'Access global logs. Ability to search/filter' },

@ -8,7 +8,7 @@ const { Bypass } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { secureCookies, blockBypass } = config.get;
const bypass = await Bypass.getBypass(res.locals.anonymizer);
const bypass = await Bypass.getBypass(res.locals.anonymizer, res.locals.pseudoIp, blockBypass.expireAfterUses);
const bypassId = bypass.insertedId;
res.locals.blockBypass = true;

@ -20,6 +20,7 @@ module.exports = async (req, res) => {
updatingPermissions.set(Permissions.BYPASS_SPAMCHECK, (req.body.BYPASS_SPAMCHECK != null));
updatingPermissions.set(Permissions.BYPASS_RATELIMITS, (req.body.BYPASS_RATELIMITS != null));
updatingPermissions.set(Permissions.BYPASS_FILTERS, (req.body.BYPASS_FILTERS != null));
updatingPermissions.set(Permissions.BYPASS_CAPTCHA, (req.body.BYPASS_CAPTCHA != null));
updatingPermissions.set(Permissions.MANAGE_GLOBAL_GENERAL, (req.body.MANAGE_GLOBAL_GENERAL != null));
updatingPermissions.set(Permissions.MANAGE_GLOBAL_BANS, (req.body.MANAGE_GLOBAL_BANS != null));
updatingPermissions.set(Permissions.MANAGE_GLOBAL_LOGS, (req.body.MANAGE_GLOBAL_LOGS != null));

Loading…
Cancel
Save