diff --git a/db/bans.js b/db/bans.js index 1ae1aeb4..7468fc48 100644 --- a/db/bans.js +++ b/db/bans.js @@ -9,8 +9,16 @@ module.exports = { db, find: (ip, board) => { + let ipQuery; + if (typeof ip === 'object') { //object with hash and ranges in bancheck + ipQuery = { + '$in': Object.values(ip) + } + } else { + ipQuery = ip; + } return db.find({ - 'ip': ip, + 'ip': ipQuery, 'board': { '$in': [board, null] } diff --git a/helpers/checks/bancheck.js b/helpers/checks/bancheck.js index 767a9d63..5da0ae49 100644 --- a/helpers/checks/bancheck.js +++ b/helpers/checks/bancheck.js @@ -14,6 +14,7 @@ module.exports = async (req, res, next) => { const allowAppeal = bans.filter(ban => ban.allowAppeal === true && ban.appeal === null).length > 0; const unseenBans = bans.filter(b => !b.seen).map(b => b._id); await Bans.markSeen(unseenBans); //mark bans as seen + bans.forEach(ban => ban.seen = true); //mark seen as true in memory for user viewed ban page return res.status(403).render('ban', { bans: bans, allowAppeal diff --git a/helpers/checks/spamcheck.js b/helpers/checks/spamcheck.js index 9c157824..fa63aac5 100644 --- a/helpers/checks/spamcheck.js +++ b/helpers/checks/spamcheck.js @@ -44,7 +44,7 @@ module.exports = async (req, res) => { '_id': { '$gt': last120id }, - 'ip': res.locals.ip, + 'ip': res.locals.ip.hash, '$or': contentOr }); //any posts from same IP in past 15 seconds @@ -52,7 +52,7 @@ module.exports = async (req, res) => { '_id': { '$gt': last15id }, - 'ip': res.locals.ip + 'ip': res.locals.ip.hash }) let flood = await Posts.db.find({ diff --git a/helpers/iphash.js b/helpers/iphash.js deleted file mode 100644 index d0501162..00000000 --- a/helpers/iphash.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const configs = require(__dirname+'/../configs/main.json') - , { createHash } = require('crypto'); - -module.exports = (req, res, next) => { - const ip = req.headers['x-real-ip']; //need to consider forwarded-for, etc here and in nginx - res.locals.ip = createHash('sha256').update(configs.ipHashSecret + ip).digest('base64'); - next(); -} diff --git a/helpers/processip.js b/helpers/processip.js new file mode 100644 index 00000000..052da4fb --- /dev/null +++ b/helpers/processip.js @@ -0,0 +1,15 @@ +'use strict'; + +const { ipHashSecret } = require(__dirname+'/../configs/main.json') + , { createHash } = require('crypto'); + +module.exports = (req, res, next) => { + const ip = req.headers['x-real-ip']; //need to consider forwarded-for, etc here and in nginx + const split = ip.split('.'); + res.locals.ip = { + hash: createHash('sha256').update(ipHashSecret + ip).digest('base64'), + qrange: createHash('sha256').update(ipHashSecret + split.slice(0,3).join('.')).digest('base64'), + hrange: createHash('sha256').update(ipHashSecret + split.slice(0,2).join('.')).digest('base64'), + } + next(); +} diff --git a/models/forms/actionhandler.js b/models/forms/actionhandler.js index 02854004..ab0a4498 100644 --- a/models/forms/actionhandler.js +++ b/models/forms/actionhandler.js @@ -98,7 +98,7 @@ module.exports = async (req, res, next) => { } if (req.body.delete || req.body.delete_ip_board || req.body.delete_ip_global) { if (req.body.delete_ip_board || req.body.delete_ip_global) { - const deletePostIps = res.locals.posts.map(x => x.ip); + const deletePostIps = res.locals.posts.map(x => x.ip.hash); let query = { 'ip': { '$in': deletePostIps diff --git a/models/forms/banposter.js b/models/forms/banposter.js index e57e4ced..22b0ab68 100644 --- a/models/forms/banposter.js +++ b/models/forms/banposter.js @@ -7,23 +7,29 @@ module.exports = async (req, res, next) => { const banDate = new Date(); const banExpiry = new Date(req.body.ban_duration ? banDate.getTime() + req.body.ban_duration : 8640000000000000); //perm if none or malformed input const banReason = req.body.ban_reason || 'No reason specified'; - const allowAppeal = req.body.no_appeal ? false : true; + const allowAppeal = (req.body.no_appeal || !req.body.ban_q || !req.body.ban_h) ? false : true; //dont allow appeals for range bans const bans = []; if (req.body.ban || req.body.global_ban) { const banBoard = req.body.global_ban ? null : req.params.board; const ipPosts = res.locals.posts.reduce((acc, post) => { - if (!acc[post.ip]) { - acc[post.ip] = []; + if (!acc[post.ip.hash]) { + acc[post.ip.hash] = []; } - acc[post.ip].push(post); + acc[post.ip.hash].push(post); return acc; }, {}); for (let ip in ipPosts) { const thisIpPosts = ipPosts[ip]; + let banIp = ip; + if (req.body.ban_h) { + banIp = thisIpPosts[0].ip.hrange; + } else if (req.body.ban_q) { + banIp = thisIpPosts[0].ip.qrange; + } bans.push({ - ip, + 'ip': banIp, 'reason': banReason, 'board': banBoard, 'posts': req.body.preserve_post ? thisIpPosts : null, diff --git a/models/forms/makepost.js b/models/forms/makepost.js index 30c49cc7..a1765b23 100644 --- a/models/forms/makepost.js +++ b/models/forms/makepost.js @@ -115,7 +115,7 @@ module.exports = async (req, res, next) => { const banDate = new Date(); const banExpiry = new Date(filterBanDuration + banDate.getTime()); const ban = { - 'ip': res.locals.ip, + 'ip': res.locals.ip.hash, 'reason': 'post word filter auto ban', 'board': res.locals.board._id, 'posts': null, @@ -126,7 +126,7 @@ module.exports = async (req, res, next) => { 'seen': false }; await Bans.insertOne(ban); - const bans = await Bans.find(res.locals.ip, res.locals.board._id); //need to query db so it has _id field for unban checkmark + const bans = await Bans.find(res.locals.ip.hash, res.locals.board._id); //need to query db so it has _id field for unban checkmark return res.status(403).render('ban', { bans: bans }); @@ -241,7 +241,7 @@ module.exports = async (req, res, next) => { salt = (await randomBytes(128)).toString('base64'); } if (ids === true) { - const fullUserIdHash = createHash('sha256').update(salt + res.locals.ip).digest('hex'); + const fullUserIdHash = createHash('sha256').update(salt + res.locals.ip.hash).digest('hex'); userId = fullUserIdHash.substring(fullUserIdHash.length-6); } let country = null; diff --git a/models/forms/reportpost.js b/models/forms/reportpost.js index 50fd0882..d07fa678 100644 --- a/models/forms/reportpost.js +++ b/models/forms/reportpost.js @@ -8,7 +8,7 @@ module.exports = (req, res) => { 'id': ObjectId(), 'reason': req.body.report_reason, 'date': new Date(), - 'ip': res.locals.ip + 'ip': res.locals.ip.hash //just hash for now, no rangeban reporters } const ret = { diff --git a/models/pages/captcha.js b/models/pages/captcha.js index 4598eae8..32acdce3 100644 --- a/models/pages/captcha.js +++ b/models/pages/captcha.js @@ -7,7 +7,7 @@ module.exports = async (req, res, next) => { let captchaId; try { - const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip, 10); + const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.hash, 10); if (ratelimit > 100) { return res.status(429).redirect('/img/ratelimit.png'); } diff --git a/server.js b/server.js index ad12e67b..b5fcc47c 100644 --- a/server.js +++ b/server.js @@ -12,7 +12,7 @@ const express = require('express') , server = require('http').createServer(app) , cookieParser = require('cookie-parser') , configs = require(__dirname+'/configs/main.json') - , ipHash = require(__dirname+'/helpers/iphash.js') + , processIp = require(__dirname+'/helpers/processip.js') , referrerCheck = require(__dirname+'/helpers/referrercheck.js') , themes = require(__dirname+'/helpers/themes.js') , Mongo = require(__dirname+'/db/db.js') @@ -62,7 +62,7 @@ const express = require('express') app.set('trust proxy', 1); //self explanatory middlewares - app.use(ipHash); + app.use(processIp); app.use(referrerCheck); // use pug view engine diff --git a/views/custompages/faq.pug b/views/custompages/faq.pug index 6aeb7cfc..9117345f 100644 --- a/views/custompages/faq.pug +++ b/views/custompages/faq.pug @@ -19,6 +19,7 @@ block content ul.mv-0 li: a(href='#whats-an-imageboard') What is an imageboard? li: a(href='/rules.html') What are the rules? + li: a(href='#site-operation') How do you run the site? b Making posts ul.mv-0 //li: a(href='#how-to-post') How do I make a post? @@ -198,3 +199,24 @@ block content li Board owner: Same as board moderator li Board moderator: All below, plus ban, delete-by-ip, sticky/sage/lock/cycle li Regular user: Reports, and post spoiler/delete/unlink if the board has them enabled + .table-container.flex-center.mv-5 + .anchor#site-operation + table + tr + th: a(href='#site-operation') How is the site run? + tr + td + b Who owns the site? + p An Australian guy called Tom. You can check the check the jschan repo for my discord and dig up more about me if you are curious. I also run some other projects including a large discord bot. + b Where is the server? + p VPS in New York, USA. + b Who has access to the server? + p Me (Tom) only. + b What OS does the server run? + p Debian 9 minimal. + b How are IP addresses stored? + p Jschan stores them hashed and salted, and a substring of this is shown in ban pages and moderation interfaces. Clear IPs are present in Nginx logs and retained for 7 days. + b Is the server secure? + p Only ports 443 and 80 are open for HTTP(s) and one other port for SSH. Key only login is enabled and root login is disabled. The software is running as an unprivileged users. MongoDB and Redis are configured to listen on local interfaces only and require authentication. + b I have an issue/found a vulnerability/need to contact you. + p Issues with the software can be posted on the #[a(href='https://github.com/fatchan/jschan/issues') issues page] or if you prefer not to use github, #[a(href='/t/index.html#postform') post on the meta board] with a detailed description of the problem. For private matters or vulnerability reports, please contact me via email tom-69420-me. diff --git a/views/includes/actionfooter.pug b/views/includes/actionfooter.pug index 50f771dc..a2a27aa3 100644 --- a/views/includes/actionfooter.pug +++ b/views/includes/actionfooter.pug @@ -38,6 +38,11 @@ details.toggle-label label input.post-check(type='checkbox', name='global_ban' value='1') | Global Ban Poster + label + input.post-check(type='checkbox', name='ban_q' value='1') + | 1/4 + input.post-check(type='checkbox', name='ban_h' value='1') + | 1/2 Range label input.post-check(type='checkbox', name='no_appeal' value='1') | Non-appealable Ban diff --git a/views/includes/actionfooter_globalmanage.pug b/views/includes/actionfooter_globalmanage.pug index 719ed16a..4d25a656 100644 --- a/views/includes/actionfooter_globalmanage.pug +++ b/views/includes/actionfooter_globalmanage.pug @@ -3,31 +3,36 @@ details.toggle-label .actions h4.no-m-p Actions: label - input.post-check(type='checkbox', name='delete' value='Delete post') + input.post-check(type='checkbox', name='delete' value='1') | Delete Posts label - input.post-check(type='checkbox', name='delete_file' value='Delete files') + input.post-check(type='checkbox', name='delete_file' value='1') | Delete Files label - input.post-check(type='checkbox', name='spoiler' value='Spoiler files') + input.post-check(type='checkbox', name='spoiler' value='1') | Spoiler Files label - input.post-check(type='checkbox', name='delete_ip_global' value='Delete posts by IP global') + input.post-check(type='checkbox', name='delete_ip_global' value='1') | Delete from IP globally label - input.post-check(type='checkbox', name='global_dismiss' value='Dismiss global reports') + input.post-check(type='checkbox', name='global_dismiss' value='1') | Dismiss Global Reports label - input.post-check(type='checkbox', name='global_report_ban' value='Global ban reporters') + input.post-check(type='checkbox', name='global_report_ban' value='1') | Global Ban Reporters label - input.post-check(type='checkbox', name='global_ban' value='Ban global') + input.post-check(type='checkbox', name='global_ban' value='1') | Global Ban Poster label - input.post-check(type='checkbox', name='no_appeal' value='Non-appealable') + input.post-check(type='checkbox', name='ban_q' value='1') + | 1/4 + input.post-check(type='checkbox', name='ban_h' value='1') + | 1/2 Range + label + input.post-check(type='checkbox', name='no_appeal' value='1') | Non-appealable Ban label - input.post-check(type='checkbox', name='preserve_post' value='Show post in ban') + input.post-check(type='checkbox', name='preserve_post' value='1') | Show Post In Ban label input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') diff --git a/views/includes/actionfooter_manage.pug b/views/includes/actionfooter_manage.pug index af679df9..c17247d2 100644 --- a/views/includes/actionfooter_manage.pug +++ b/views/includes/actionfooter_manage.pug @@ -3,42 +3,47 @@ details.toggle-label .actions h4.no-m-p Actions: label - input.post-check(type='checkbox', name='delete' value='Delete post') + input.post-check(type='checkbox', name='delete' value='1') | Delete Posts label - input.post-check(type='checkbox', name='delete_file' value='Delete files') + input.post-check(type='checkbox', name='delete_file' value='1') | Delete Files label - input.post-check(type='checkbox', name='spoiler' value='Spoiler files') + input.post-check(type='checkbox', name='spoiler' value='1') | Spoiler Files label - input.post-check(type='checkbox', name='global_report' value='Global report') + input.post-check(type='checkbox', name='global_report' value='1') | Global Report label input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') label - input.post-check(type='checkbox', name='delete_ip_board' value='Delete posts by IP') + input.post-check(type='checkbox', name='delete_ip_board' value='1') | Delete from IP on board label - input.post-check(type='checkbox', name='delete_ip_global' value='Delete posts by IP global') + input.post-check(type='checkbox', name='delete_ip_global' value='1') | Delete from IP globally label - input.post-check(type='checkbox', name='dismiss' value='Dismiss reports') + input.post-check(type='checkbox', name='dismiss' value='1') | Dismiss Reports label - input.post-check(type='checkbox', name='report_ban' value='Ban reporters') + input.post-check(type='checkbox', name='report_ban' value='1') | Ban Reporters label - input.post-check(type='checkbox', name='ban' value='Ban') + input.post-check(type='checkbox', name='ban' value='1') | Ban Poster label - input.post-check(type='checkbox', name='global_ban' value='Ban global') + input.post-check(type='checkbox', name='global_ban' value='1') | Global Ban Poster label - input.post-check(type='checkbox', name='no_appeal' value='Non-appealable') + input.post-check(type='checkbox', name='ban_q' value='1') + | 1/4 + input.post-check(type='checkbox', name='ban_h' value='1') + | 1/2 Range + label + input.post-check(type='checkbox', name='no_appeal' value='1') | Non-appealable Ban label - input.post-check(type='checkbox', name='preserve_post' value='Show post in ban') + input.post-check(type='checkbox', name='preserve_post' value='1') | Show Post In Ban label input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') diff --git a/views/mixins/ban.pug b/views/mixins/ban.pug index ba91ec4a..96d0cb45 100644 --- a/views/mixins/ban.pug +++ b/views/mixins/ban.pug @@ -37,3 +37,5 @@ mixin ban(ban, banpage) textarea(rows=1 disabled='true') #{ban.appeal} else if ban.allowAppeal | No appeal submitted + else + | -