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

merge-requests/345/merge
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 }) , router = express.Router({ caseSensitive: true })
, Boards = require(__dirname+'/../db/boards.js') , Boards = require(__dirname+'/../db/boards.js')
//middlewares //middlewares
, torPreBypassCheck = require(__dirname+'/../lib/middleware/captcha/torprebypass.js') , geoIp = require(__dirname+'/../lib/middleware/ip/geoip.js')
, geoAndTor = require(__dirname+'/../lib/middleware/ip/geoip.js')
, processIp = require(__dirname+'/../lib/middleware/ip/processip.js') , processIp = require(__dirname+'/../lib/middleware/ip/processip.js')
, calcPerms = require(__dirname+'/../lib/middleware/permission/calcpermsmiddleware.js') , calcPerms = require(__dirname+'/../lib/middleware/permission/calcpermsmiddleware.js')
, Permissions = require(__dirname+'/../lib/permission/permissions.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'); editRoleController, newCaptchaForm, blockBypassForm, logoutForm, deleteSessionsController } = require(__dirname+'/forms/index.js');
//make new post //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); 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 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 //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/actions', geoIp, 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/modactions', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), actionController.paramConverter, actionController.controller); //board manage page 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 hasPerms.one(Permissions.MANAGE_GLOBAL_GENERAL), globalActionController.paramConverter, globalActionController.controller); //global manage page
//appeal ban //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 //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); hasPerms.any(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL), editPostController.controller);
//board management forms //board management forms
router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, 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); 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); hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), boardSettingsController.paramConverter, boardSettingsController.controller);
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn, router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_BANS), editBansController.paramConverter, editBansController.controller); //edit bans 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 hasPerms.one(Permissions.MANAGE_GLOBAL_SETTINGS), globalSettingsController.paramConverter, globalSettingsController.controller); //global settings
//create board //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 //accounts
router.post('/login', useSession, loginController.paramConverter, loginController.controller); router.post('/login', useSession, loginController.paramConverter, loginController.controller);
router.post('/logout', useSession, logoutForm); router.post('/logout', useSession, logoutForm);
router.post('/register', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, calcPerms, verifyCaptcha, registerController.paramConverter, registerController.controller); router.post('/register', geoIp, processIp, useSession, sessionRefresh, calcPerms, verifyCaptcha, registerController.paramConverter, registerController.controller);
router.post('/changepassword', geoAndTor, torPreBypassCheck, processIp, useSession, sessionRefresh, verifyCaptcha, changePasswordController.paramConverter, changePasswordController.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('/resign', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, resignController.paramConverter, resignController.controller);
router.post('/deleteaccount', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteAccountController.controller); router.post('/deleteaccount', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteAccountController.controller);
router.post('/deletesessions', useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, deleteSessionsController.paramConverter, deleteSessionsController.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 //removes captcha cookie, for refreshing for noscript users
router.post('/newcaptcha', newCaptchaForm); router.post('/newcaptcha', newCaptchaForm);
//solve captcha for block bypass //solve captcha for block bypass
router.post('/blockbypass', geoAndTor, processIp, verifyCaptcha, blockBypassForm); router.post('/blockbypass', geoIp, processIp, verifyCaptcha, blockBypassForm);
module.exports = router; module.exports = router;

@ -6,7 +6,7 @@ const express = require('express')
, Posts = require(__dirname+'/../db/posts.js') , Posts = require(__dirname+'/../db/posts.js')
//middlewares //middlewares
, processIp = require(__dirname+'/../lib/middleware/ip/processip.js') , 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') , calcPerms = require(__dirname+'/../lib/middleware/permission/calcpermsmiddleware.js')
, Permissions = require(__dirname+'/../lib/permission/permissions.js') , Permissions = require(__dirname+'/../lib/permission/permissions.js')
, hasPerms = require(__dirname+'/../lib/middleware/permission/haspermsmiddleware.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 //TODO: edit post edit page form, like editnews/editaccount/editrole endpoint
//captcha //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('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page router.get('/bypass.html', blockBypass); //block bypass page
router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page

@ -2,34 +2,38 @@
const Mongo = require(__dirname+'/db.js') const Mongo = require(__dirname+'/db.js')
, config = require(__dirname+'/../lib/misc/config.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 = { module.exports = {
db, db,
checkBypass: (id, anonymizer=false) => { checkBypass: (id, anonymizer=false) => {
const { blockBypass } = config.get;
return db.findOneAndUpdate({ return db.findOneAndUpdate({
'_id': id, '_id': id,
'anonymizer': anonymizer, 'anonymizer': anonymizer,
'uses': { 'uses': {
'$lte': blockBypass.expireAfterUses '$gt': 0
} }
}, { }, {
'$inc': { '$inc': {
'uses': 1, 'uses': -1,
} }
}).then(r => r.value); }).then(r => r.value);
}, },
getBypass: (anonymizer=false) => { getBypass: (anonymizer=false, id=null, uses=0) => {
const { blockBypass } = config.get; const { blockBypass } = config.get;
return db.insertOne({ const newBypass = {
'uses': 0, 'uses': uses,
'anonymizer': anonymizer, 'anonymizer': anonymizer,
'expireAt': new Date(Date.now() + blockBypass.expireAfterTime) 'expireAt': new Date(Date.now() + blockBypass.expireAfterTime)
}); };
if (anonymizer === true && id !== null) {
newBypass._id = Mongo.ObjectId(id);
}
return db.insertOne(newBypass);
}, },
deleteAll: () => { deleteAll: () => {

@ -10,6 +10,7 @@ const { Bypass } = require(__dirname+'/../../../db/')
module.exports = { module.exports = {
check: async (req, res, next) => { check: async (req, res, next) => {
const { secureCookies, blockBypass } = config.get; const { secureCookies, blockBypass } = config.get;
//check if blockbypass exists and right length //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; let bypass;
if (bypassId && bypassId.length === 24) { if (bypassId && bypassId.length === 24) {
try { try {
const bypassMongoId = ObjectId(bypassId); const bypassMongoId = ObjectId(bypassId);
bypass = await Bypass.checkBypass(bypassMongoId, res.locals.anonymizer); bypass = await Bypass.checkBypass(bypassMongoId, res.locals.anonymizer);
res.locals.blockBypass = true;
} catch (err) { } catch (err) {
return next(err); return next(err);
} }
} }
if (bypass //if they have a valid bypass //next if they have a valid bypass
&& (bypass.uses < blockBypass.expireAfterUses //and its not overused if (bypass != null) {
|| (res.locals.anonymizer res.locals.blockBypass = true;
&& !blockBypass.forceAnonymizers))) { //OR its not forced for anonymizers
return next(); return next();
} }
if (res.locals.solvedCaptcha) { if (res.locals.solvedCaptcha) {
//they dont have a valid bypass, but just solved board captcha, so give them a new one //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; const newBypassId = newBypass.insertedId;
res.locals.blockBypass = true; res.locals.blockBypass = true;
res.cookie('bypassid', newBypassId.toString(), { res.cookie('bypassid', newBypassId.toString(), {
@ -61,6 +60,7 @@ module.exports = {
} }
deleteTempFiles(req).catch(console.error); deleteTempFiles(req).catch(console.error);
res.clearCookie('bypassid');
return dynamicResponse(req, res, 403, 'message', { return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden', 'title': 'Forbidden',
'message': 'Block bypass expired or exceeded max uses', 'message': 'Block bypass expired or exceeded max uses',
@ -74,11 +74,19 @@ module.exports = {
}, },
middleware: async (req, res, next) => { middleware: async (req, res, next) => {
const { blockBypass } = config.get; const { blockBypass, secureCookies } = config.get;
if (res.locals.preFetchedBypassId //if they already have a bypass //skip bypass check if not enabled, and not forced for anonymizer or we arent an anonymizer
|| (!blockBypass.enabled //or if block bypass isnt enabled if (!blockBypass.enabled &&
&& (!blockBypass.forceAnonymizers //and we dont force it for anonymizer (!blockBypass.forceAnonymizers || !res.locals.anonymizer)) {
|| !res.locals.anonymizer))) { //or they arent on an 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 next();
} }
return module.exports.check(req, res, 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 //bypass captcha permission
//console.log(res.locals.permissions.toJSON()); if (res.locals.permissions &&
if (res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) { res.locals.permissions.get(Permissions.BYPASS_CAPTCHA)) {
res.locals.solvedCaptcha = true; res.locals.solvedCaptcha = true;
return next(); return next();
} }

@ -69,16 +69,7 @@ module.exports = {
return fileHandlers.flag(req, res, next); return fileHandlers.flag(req, res, next);
}, },
posts: (req, res, next) => { posts: (req, res, next) => {
if (res.locals.anonymizer) {
return next();
}
return fileHandlers.post(req, res, 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') const config = require(__dirname+'/../../misc/config.js')
, { createCIDR, parse } = require('ip6addr') , { 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) => { 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) { 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 = { res.locals.ip = {
raw: `${pseudoIp}.BP`, raw: `${pseudoIp}.BP`,
cloak: `${pseudoIp}.BP`, cloak: `${pseudoIp}.BP`,

@ -8,7 +8,7 @@ const { Bypass } = require(__dirname+'/../../db/')
module.exports = async (req, res) => { module.exports = async (req, res) => {
const { secureCookies, blockBypass } = config.get; 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; const bypassId = bypass.insertedId;
res.locals.blockBypass = true; res.locals.blockBypass = true;

Loading…
Cancel
Save