mirror of
https://github.com/luxfi/pics.git
synced 2026-07-26 21:08:58 +00:00
Add a global option to force accounts to require 2fa
Add a global option to force changing board settings to need 2FA
This commit is contained in:
@@ -62,6 +62,10 @@ module.exports = {
|
||||
cacheTime: 3600 //in seconds, idk whats a good value
|
||||
},
|
||||
|
||||
//TODO: default these true and update instructions for how to disable
|
||||
forceAccountTwofactor: false,
|
||||
forceActionTwofactor: false,
|
||||
|
||||
//disable file posting over anonymizers globally, overrides any board setting.
|
||||
disableAnonymizerFilePosting: false,
|
||||
|
||||
|
||||
@@ -8,14 +8,15 @@ const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardse
|
||||
, config = require(__dirname+'/../../lib/misc/config.js')
|
||||
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
|
||||
, i18n = require(__dirname+'/../../lib/locale/locale.js')
|
||||
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
|
||||
, doTwoFactor = require(__dirname+'/../../lib/misc/dotwofactor.js')
|
||||
, { existsBody, checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
|
||||
inArrayBody, arrayInBody } = require(__dirname+'/../../lib/input/schema.js');
|
||||
|
||||
module.exports = {
|
||||
|
||||
paramConverter: paramConverter({
|
||||
timeFields: ['delete_protection_age'],
|
||||
trimFields: ['tags', 'announcement', 'description', 'name', 'custom_css', 'language'],
|
||||
trimFields: ['twofactor', 'tags', 'announcement', 'description', 'name', 'custom_css', 'language'],
|
||||
allowedArrays: ['countries'],
|
||||
numberFields: ['lock_reset', 'captcha_reset', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'pph_trigger_action',
|
||||
'tph_trigger_action', 'bump_limit', 'reply_limit', 'max_files', 'thread_limit', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length',
|
||||
@@ -26,11 +27,23 @@ module.exports = {
|
||||
|
||||
const { __ } = res.locals;
|
||||
|
||||
const { globalLimits, rateLimitCost } = config.get
|
||||
const { globalLimits, rateLimitCost, forceActionTwofactor } = config.get
|
||||
, maxThread = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message)
|
||||
, maxReply = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message);
|
||||
|
||||
const errors = await checkSchema([
|
||||
{ result: existsBody(req.body.twofactor) ? lengthBody(req.body.twofactor, 0, 6) : false, expected: false, error: __('Invalid 2FA code') },
|
||||
{ result: async () => {
|
||||
if (res.locals.user.twofactor && forceActionTwofactor) {
|
||||
//2fA (TOTP) validation
|
||||
const delta = await doTwoFactor(res.locals.user.username, res.locals.user.twofactor, req.body.twofactor || '');
|
||||
if (delta === null) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true; //Force twofactor not enabled
|
||||
}
|
||||
}, expected: true, error: __('Invalid 2FA Code') },
|
||||
{ result: lengthBody(req.body.description, 0, globalLimits.fieldLength.description), expected: false, error: __('Board description must be %s characters or less', globalLimits.fieldLength.description) },
|
||||
{ result: lengthBody(req.body.announcements, 0, 5000), expected: false, error: __('Board announcements must be 5000 characters or less') },
|
||||
{ result: lengthBody(req.body.tags, 0, 2000), expected: false, error: __('Tags length must be 2000 characters or less') },
|
||||
|
||||
@@ -7,6 +7,7 @@ const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobal
|
||||
, { fontPaths } = require(__dirname+'/../../lib/misc/fonts.js')
|
||||
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
|
||||
, i18n = require(__dirname+'/../../lib/locale/locale.js')
|
||||
, doTwoFactor = require(__dirname+'/../../lib/misc/dotwofactor.js')
|
||||
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
|
||||
inArrayBody, existsBody } = require(__dirname+'/../../lib/input/schema.js');
|
||||
|
||||
@@ -14,7 +15,7 @@ module.exports = {
|
||||
|
||||
paramConverter: paramConverter({
|
||||
timeFields: ['hot_threads_max_age', 'inactive_account_time', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time', 'board_defaults_delete_protection_age'],
|
||||
trimFields: ['captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses', 'captcha_options_font', 'allowed_hosts', 'dnsbl_blacklists', 'other_mime_types',
|
||||
trimFields: ['twofactor', 'captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses', 'captcha_options_font', 'allowed_hosts', 'dnsbl_blacklists', 'other_mime_types',
|
||||
'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'ethereum_links', 'reverse_links', 'language', 'board_defaults_language'],
|
||||
numberFields: ['inactive_account_action', 'abandoned_board_action', 'auth_level', 'captcha_options_text_wave', 'captcha_options_text_paint', 'captcha_options_text_noise',
|
||||
'captcha_options_grid_noise', 'captcha_options_grid_edge', 'captcha_options_generate_limit', 'captcha_options_grid_size', 'captcha_options_grid_image_size',
|
||||
@@ -41,9 +42,26 @@ module.exports = {
|
||||
|
||||
const { __ } = res.locals;
|
||||
|
||||
const { globalLimits } = config.get;
|
||||
const { globalLimits, forceActionTwofactor } = config.get;
|
||||
|
||||
const errors = await checkSchema([
|
||||
{ result: existsBody(req.body.twofactor) ? lengthBody(req.body.twofactor, 0, 6) : false, expected: false, error: __('Invalid 2FA code') },
|
||||
{ result: async () => {
|
||||
if (res.locals.user.twofactor && forceActionTwofactor) {
|
||||
//2fA (TOTP) validation
|
||||
try {
|
||||
const delta = await doTwoFactor(res.locals.user.username, res.locals.user.twofactor, req.body.twofactor || '');
|
||||
if (delta === null) {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
// console.warn(err);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true; //Force twofactor not enabled
|
||||
}
|
||||
}, expected: true, error: __('Invalid 2FA Code') },
|
||||
{ result: () => {
|
||||
if (req.body.thumb_extension) {
|
||||
return /\.[a-z0-9]+/i.test(req.body.thumb_extension);
|
||||
@@ -209,6 +227,7 @@ module.exports = {
|
||||
{ result: lengthBody(req.body.webring_following, 0, 10000), expected: false, error: __('Webring following list must not exceed 10000 characters') },
|
||||
{ result: lengthBody(req.body.webring_blacklist, 0, 10000), expected: false, error: __('Webring blacklist must not exceed 10000 characters') },
|
||||
{ result: lengthBody(req.body.webring_logos, 0, 10000), expected: false, error: __('Webring logos list must not exceed 10000 characters') },
|
||||
{ result: lengthBody(req.body.twofactor, 0, 6), expected: false, error: __('Invalid 2FA code') },
|
||||
]);
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
const { __ } = res.locals;
|
||||
|
||||
const errors = await checkSchema([
|
||||
{ result: res.locals.user.twofactor === false, expected: true, error: __('You already have 2FA setup') },
|
||||
{ result: res.locals.user.twofactor === null, expected: false, error: __('You already have 2FA setup') },
|
||||
{ result: existsBody(req.body.twofactor), expected: true, error: __('Missing 2FA code') },
|
||||
{ result: lengthBody(req.body.twofactor, 6, 6), expected: false, error: __('2FA code must be 6 characters') },
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
const { Accounts } = require(__dirname+'/../../../db/')
|
||||
, { DAY } = require(__dirname+'/../../converter/timeutils.js')
|
||||
, cache = require(__dirname+'/../../redis/redis.js');
|
||||
, cache = require(__dirname+'/../../redis/redis.js')
|
||||
, config = require(__dirname+'/../../misc/config.js');
|
||||
|
||||
module.exports = async (req, res, next) => {
|
||||
if (!res.locals) {
|
||||
@@ -29,6 +30,14 @@ module.exports = async (req, res, next) => {
|
||||
cache.set(`users:${req.session.user}`, res.locals.user, 3600);
|
||||
}
|
||||
}
|
||||
const { forceAccountTwofactor } = config.get;
|
||||
//Note: not /forms/twofactor because req.path doesnt contain the mount point when called from a middleware
|
||||
if (!['/twofactor.html', '/twofactor'].includes(req.path)
|
||||
&& forceAccountTwofactor === true
|
||||
&& res.locals.user
|
||||
&& !res.locals.user.twofactor) {
|
||||
return res.redirect('/twofactor.html');
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = async(db, redis) => {
|
||||
|
||||
console.log('Updating globalsettings to add 2fa enforcement options');
|
||||
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
|
||||
'$set': {
|
||||
'forceAccountTwofactor': false,
|
||||
'forceActionTwofactor': false, //TODO: potentially break this down to be more granular on what needs 2fa
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Clearing globalsettings cache');
|
||||
await redis.deletePattern('globalsettings');
|
||||
console.log('Clearing boards cache');
|
||||
await redis.deletePattern('board:*');
|
||||
|
||||
};
|
||||
@@ -119,6 +119,8 @@ module.exports = async (req, res) => {
|
||||
boardSettings: numberSetting(req.body.rate_limit_cost_board_settings, oldSettings.rateLimitCost.boardSettings),
|
||||
editPost: numberSetting(req.body.rate_limit_cost_edit_post, oldSettings.rateLimitCost.editPost),
|
||||
},
|
||||
forceAccountTwofactor: booleanSetting(req.body.force_account_twofactor, oldSettings.forceAccountTwofactor),
|
||||
forceActionTwofactor: booleanSetting(req.body.force_action_twofactor, oldSettings.forceActionTwofactor),
|
||||
inactiveAccountTime: numberSetting(req.body.inactive_account_time, oldSettings.inactiveAccountTime),
|
||||
inactiveAccountAction: numberSetting(req.body.inactive_account_action, oldSettings.inactiveAccountAction),
|
||||
abandonedBoardAction: numberSetting(req.body.abandoned_board_action, oldSettings.abandonedBoardAction),
|
||||
@@ -342,11 +344,16 @@ module.exports = async (req, res) => {
|
||||
|
||||
await Mongo.setConfig(newSettings);
|
||||
|
||||
//Toggling forced 2fa modes requires user cache to be cleared so new twofactor value can be fetched during refreshsession()
|
||||
if (oldSettings.forceAccountTwofactor != newSettings.forceAccountTwofactor) {
|
||||
promises.push(redis.deletePattern('users:*'));
|
||||
}
|
||||
|
||||
//webring being disabled
|
||||
if (oldSettings.enableWebring === true && newSettings.enableWebring === false) {
|
||||
promises.push(Boards.db.deleteMany({ webring: true }));
|
||||
promises.push(remove(`${uploadDirectory}/json/webring.json`));
|
||||
redis.del('webringsites');
|
||||
promises.push(redis.del('webringsites'));
|
||||
}
|
||||
|
||||
//finish the promises in parallel e.g. removing files
|
||||
|
||||
@@ -40,7 +40,7 @@ module.exports = async (req, res) => {
|
||||
// bcrypt compare input to saved hash
|
||||
const passwordMatch = await bcrypt.compare(password, account.passwordHash);
|
||||
//2fA (TOTP) validation
|
||||
const delta = await doTwoFactor(username, account.twofactor, req.body.twofactor || '');
|
||||
const delta = await doTwoFactor(username, account.twofactor || '', req.body.twofactor || '');
|
||||
//if password was correct and 2fa valid (if enabled)
|
||||
if (passwordMatch === false
|
||||
|| (account.twofactor && delta === null)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = async (req, res, next) => {
|
||||
|
||||
// Ratelimit QR code generation
|
||||
const username = res.locals.user.username;
|
||||
const ratelimit = await Ratelimits.incrmentQuota(username, '2fa', 50);
|
||||
const ratelimit = await Ratelimits.incrmentQuota(username, '2fa', 10);
|
||||
if (ratelimit > 100) {
|
||||
const { __ } = res.locals;
|
||||
return dynamicResponse(req, res, 429, 'message', {
|
||||
|
||||
+2
-6
@@ -58,11 +58,7 @@ module.exports = () => describe('login and create test board', () => {
|
||||
redirect: 'manual',
|
||||
};
|
||||
const response1 = await fetch('http://localhost/forms/create', options);
|
||||
expect([302, 409]).toContain(response1.status);
|
||||
params.set('name', 'test2');
|
||||
params.set('uri', 'test2');
|
||||
const response2 = await fetch('http://localhost/forms/create', options);
|
||||
expect([302, 409]).toContain(response2.status);
|
||||
expect([302]).toContain(response1.status);
|
||||
});
|
||||
|
||||
test('create /test2/ board', async () => {
|
||||
@@ -94,7 +90,7 @@ module.exports = () => describe('login and create test board', () => {
|
||||
body: params,
|
||||
redirect: 'manual',
|
||||
});
|
||||
expect([302, 409]).toContain(response.status);
|
||||
expect([302]).toContain(response.status);
|
||||
});
|
||||
|
||||
test('change global settings, disable antispam', async () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ block content
|
||||
li: a(href='#make-a-board') How do I make my own board?
|
||||
li: a(href='#antispam') What do the board settings for antispam do?
|
||||
li: a(href='#archive-reverse-url-format') What is the archive/reverse image search link url format?
|
||||
li: a(href='#twofactor-enforcement') How does 2FA enforcement work?
|
||||
.table-container.flex-center.mv-5
|
||||
.anchor#whats-an-imageboard
|
||||
table
|
||||
@@ -289,6 +290,17 @@ block content
|
||||
| where the url of the page/file should go for reverse image search or archive links. For example
|
||||
span.mono https://tineye.com/search?url=#[span.bold %s]
|
||||
| .
|
||||
.table-container.flex-center.mv-5
|
||||
.anchor#twofactor-enforcement
|
||||
table
|
||||
tr
|
||||
th: a(href='#twofactor-enforcement') How does 2FA enforcement work?
|
||||
tr
|
||||
td
|
||||
p There are two options included in global management settings that allow admins to increase security by enforcing the use of 2FA for all accounts and/or actions.
|
||||
p Force Account 2FA: Forces all accounts to have 2FA, or else the user is redirected to the 2FA setup page.
|
||||
p Force Action 2FA: If an account has 2FA enabled, changing board settings or deleting a board requires 2FA.
|
||||
p The granularity of what actions require 2FA will be expanded and customisable in a future release.
|
||||
.table-container.flex-center.mv-5
|
||||
.anchor#contact
|
||||
table
|
||||
|
||||
@@ -135,10 +135,24 @@ ul.tabs.group
|
||||
.row
|
||||
.label #{__('Use Socks Proxy')}
|
||||
label.postform-style.ph-5
|
||||
input(type='checkbox', name='proxy_enabled', value='true' placeholder='socks5h://127.0.0.1:9050' checked=settings.proxy.enabled)
|
||||
input(type='checkbox', name='proxy_enabled', value='true' checked=settings.proxy.enabled)
|
||||
.row
|
||||
.label #{__('Proxy Address')}
|
||||
input(type='text', name='proxy_address', value=settings.proxy.address)
|
||||
input(type='text' name='proxy_address' placeholder='socks5h://127.0.0.1:9050' value=settings.proxy.address)
|
||||
.row
|
||||
.label #{__('Force Account 2FA')}
|
||||
label.postform-style.ph-5
|
||||
input(type='checkbox' name='force_account_twofactor' value='true' checked=settings.forceAccountTwofactor)
|
||||
.row
|
||||
.label
|
||||
| #{__('Force Action 2FA')}
|
||||
|
|
||||
small
|
||||
| (
|
||||
a(href='/faq.html#twofactor-enforcement') ?
|
||||
| )
|
||||
label.postform-style.ph-5
|
||||
input(type='checkbox' name='force_action_twofactor' value='true' checked=settings.forceActionTwofactor)
|
||||
|
||||
.tab.tab-3
|
||||
.col
|
||||
|
||||
@@ -27,6 +27,10 @@ block content
|
||||
.sm#tab-11
|
||||
.tabbed-area
|
||||
include ../includes/globalmanagesettings.pug
|
||||
if settings.forceActionTwofactor === true
|
||||
.row
|
||||
.label #{__('2FA Code')}
|
||||
input(type='text' name='twofactor' required)
|
||||
input.row(type='submit', value=__('Save settings'))
|
||||
hr(size=1)
|
||||
h4.no-m-p #{__('Delete board')}:
|
||||
@@ -34,7 +38,7 @@ block content
|
||||
form.form-post(action=`/forms/global/deleteboard`, enctype='application/x-www-form-urlencoded', method='POST')
|
||||
input(type='hidden' name='_csrf' value=csrf)
|
||||
.row
|
||||
.label #{__('Board URI')}:
|
||||
.label #{__('Board URI')}
|
||||
input(type='text' name='uri' required)
|
||||
.row
|
||||
.label #{__("I'm sure")}
|
||||
|
||||
Reference in New Issue
Block a user