diff --git a/controllers/forms.js b/controllers/forms.js index 09e88c9a..406f4e4d 100644 --- a/controllers/forms.js +++ b/controllers/forms.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; diff --git a/controllers/pages.js b/controllers/pages.js index a49563ed..5622c5ab 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -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 diff --git a/db/bypass.js b/db/bypass.js index 0329a0a6..a9ced9a5 100644 --- a/db/bypass.js +++ b/db/bypass.js @@ -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: () => { diff --git a/lib/captcha/captcha.js b/lib/captcha/captcha.js index 32b13e5d..a9ff6e55 100644 --- a/lib/captcha/captcha.js +++ b/lib/captcha/captcha.js @@ -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') diff --git a/lib/middleware/captcha/blockbypass.js b/lib/middleware/captcha/blockbypass.js index 164fdde7..d9fe69c4 100644 --- a/lib/middleware/captcha/blockbypass.js +++ b/lib/middleware/captcha/blockbypass.js @@ -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); diff --git a/lib/middleware/captcha/torprebypass.js b/lib/middleware/captcha/torprebypass.js deleted file mode 100644 index 7ff3b663..00000000 --- a/lib/middleware/captcha/torprebypass.js +++ /dev/null @@ -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(); - -}; diff --git a/lib/middleware/captcha/verify.js b/lib/middleware/captcha/verify.js index 0a6e5e41..15b76e90 100644 --- a/lib/middleware/captcha/verify.js +++ b/lib/middleware/captcha/verify.js @@ -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`) { diff --git a/lib/middleware/file/filemiddlewares.js b/lib/middleware/file/filemiddlewares.js index 690dfc38..8dbdc169 100644 --- a/lib/middleware/file/filemiddlewares.js +++ b/lib/middleware/file/filemiddlewares.js @@ -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(); - }, }; diff --git a/lib/middleware/ip/processip.js b/lib/middleware/ip/processip.js index 183df886..ac1f8a24 100644 --- a/lib/middleware/ip/processip.js +++ b/lib/middleware/ip/processip.js @@ -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`, diff --git a/lib/permission/permissions.js b/lib/permission/permissions.js index e70b2213..c5887519 100644 --- a/lib/permission/permissions.js +++ b/lib/permission/permissions.js @@ -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, diff --git a/lib/permission/permissiontext.js b/lib/permission/permissiontext.js index 9a6669dc..640a657c 100644 --- a/lib/permission/permissiontext.js +++ b/lib/permission/permissiontext.js @@ -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' }, diff --git a/models/forms/blockbypass.js b/models/forms/blockbypass.js index 6d870f2a..16bce877 100644 --- a/models/forms/blockbypass.js +++ b/models/forms/blockbypass.js @@ -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; diff --git a/models/forms/editaccount.js b/models/forms/editaccount.js index 6cc367a8..24da011b 100644 --- a/models/forms/editaccount.js +++ b/models/forms/editaccount.js @@ -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));