Fix the anonymizer bypass captcha permission issue (and fix the stupid pre-bypass and postsEarly in general)

merge-requests/341/head
Thomas Lynch 2 years ago
parent 3a4737ad8e
commit 47083e149b
  1. 27
      controllers/forms.js
  2. 4
      controllers/pages.js
  3. 20
      db/bypass.js
  4. 32
      lib/middleware/captcha/blockbypass.js
  5. 83
      lib/middleware/captcha/torprebypass.js
  6. 4
      lib/middleware/captcha/verify.js
  7. 9
      lib/middleware/file/filemiddlewares.js
  8. 8
      lib/middleware/ip/processip.js
  9. 2
      models/forms/blockbypass.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, calcPerms, verifyCaptcha, 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, calcPerms, verifyCaptcha, 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, 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

@ -2,34 +2,38 @@
const Mongo = require(__dirname+'/db.js')
, config = require(__dirname+'/../lib/misc/config.js')
, db = Mongo.db.collection('bypass');
, db = Mongo.db.collection('bypass')
, { ObjectId } = require(__dirname+'/../db/db.js');
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: () => {

@ -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();
};

@ -17,8 +17,8 @@ module.exports = async (req, res, next) => {
}
//bypass captcha permission
//console.log(res.locals.permissions.toJSON());
if (res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) {
if (res.locals.permissions &&
res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) {
res.locals.solvedCaptcha = true;
return next();
}

@ -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`,

@ -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;

Loading…
Cancel
Save