add ip range bans

merge-requests/208/head
fatchan 5 years ago
parent bf7796368c
commit 48e761be46
  1. 10
      db/bans.js
  2. 1
      helpers/checks/bancheck.js
  3. 4
      helpers/checks/spamcheck.js
  4. 10
      helpers/iphash.js
  5. 15
      helpers/processip.js
  6. 2
      models/forms/actionhandler.js
  7. 16
      models/forms/banposter.js
  8. 6
      models/forms/makepost.js
  9. 2
      models/forms/reportpost.js
  10. 2
      models/pages/captcha.js
  11. 4
      server.js
  12. 22
      views/custompages/faq.pug
  13. 5
      views/includes/actionfooter.pug
  14. 23
      views/includes/actionfooter_globalmanage.pug
  15. 29
      views/includes/actionfooter_manage.pug
  16. 2
      views/mixins/ban.pug

@ -9,8 +9,16 @@ module.exports = {
db, db,
find: (ip, board) => { 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({ return db.find({
'ip': ip, 'ip': ipQuery,
'board': { 'board': {
'$in': [board, null] '$in': [board, null]
} }

@ -14,6 +14,7 @@ module.exports = async (req, res, next) => {
const allowAppeal = bans.filter(ban => ban.allowAppeal === true && ban.appeal === null).length > 0; const allowAppeal = bans.filter(ban => ban.allowAppeal === true && ban.appeal === null).length > 0;
const unseenBans = bans.filter(b => !b.seen).map(b => b._id); const unseenBans = bans.filter(b => !b.seen).map(b => b._id);
await Bans.markSeen(unseenBans); //mark bans as seen 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', { return res.status(403).render('ban', {
bans: bans, bans: bans,
allowAppeal allowAppeal

@ -44,7 +44,7 @@ module.exports = async (req, res) => {
'_id': { '_id': {
'$gt': last120id '$gt': last120id
}, },
'ip': res.locals.ip, 'ip': res.locals.ip.hash,
'$or': contentOr '$or': contentOr
}); });
//any posts from same IP in past 15 seconds //any posts from same IP in past 15 seconds
@ -52,7 +52,7 @@ module.exports = async (req, res) => {
'_id': { '_id': {
'$gt': last15id '$gt': last15id
}, },
'ip': res.locals.ip 'ip': res.locals.ip.hash
}) })
let flood = await Posts.db.find({ let flood = await Posts.db.find({

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

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

@ -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 || req.body.delete_ip_board || req.body.delete_ip_global) {
if (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 = { let query = {
'ip': { 'ip': {
'$in': deletePostIps '$in': deletePostIps

@ -7,23 +7,29 @@ module.exports = async (req, res, next) => {
const banDate = new Date(); 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 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 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 = []; const bans = [];
if (req.body.ban || req.body.global_ban) { if (req.body.ban || req.body.global_ban) {
const banBoard = req.body.global_ban ? null : req.params.board; const banBoard = req.body.global_ban ? null : req.params.board;
const ipPosts = res.locals.posts.reduce((acc, post) => { const ipPosts = res.locals.posts.reduce((acc, post) => {
if (!acc[post.ip]) { if (!acc[post.ip.hash]) {
acc[post.ip] = []; acc[post.ip.hash] = [];
} }
acc[post.ip].push(post); acc[post.ip.hash].push(post);
return acc; return acc;
}, {}); }, {});
for (let ip in ipPosts) { for (let ip in ipPosts) {
const thisIpPosts = ipPosts[ip]; 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({ bans.push({
ip, 'ip': banIp,
'reason': banReason, 'reason': banReason,
'board': banBoard, 'board': banBoard,
'posts': req.body.preserve_post ? thisIpPosts : null, 'posts': req.body.preserve_post ? thisIpPosts : null,

@ -115,7 +115,7 @@ module.exports = async (req, res, next) => {
const banDate = new Date(); const banDate = new Date();
const banExpiry = new Date(filterBanDuration + banDate.getTime()); const banExpiry = new Date(filterBanDuration + banDate.getTime());
const ban = { const ban = {
'ip': res.locals.ip, 'ip': res.locals.ip.hash,
'reason': 'post word filter auto ban', 'reason': 'post word filter auto ban',
'board': res.locals.board._id, 'board': res.locals.board._id,
'posts': null, 'posts': null,
@ -126,7 +126,7 @@ module.exports = async (req, res, next) => {
'seen': false 'seen': false
}; };
await Bans.insertOne(ban); 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', { return res.status(403).render('ban', {
bans: bans bans: bans
}); });
@ -241,7 +241,7 @@ module.exports = async (req, res, next) => {
salt = (await randomBytes(128)).toString('base64'); salt = (await randomBytes(128)).toString('base64');
} }
if (ids === true) { 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); userId = fullUserIdHash.substring(fullUserIdHash.length-6);
} }
let country = null; let country = null;

@ -8,7 +8,7 @@ module.exports = (req, res) => {
'id': ObjectId(), 'id': ObjectId(),
'reason': req.body.report_reason, 'reason': req.body.report_reason,
'date': new Date(), 'date': new Date(),
'ip': res.locals.ip 'ip': res.locals.ip.hash //just hash for now, no rangeban reporters
} }
const ret = { const ret = {

@ -7,7 +7,7 @@ module.exports = async (req, res, next) => {
let captchaId; let captchaId;
try { try {
const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip, 10); const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.hash, 10);
if (ratelimit > 100) { if (ratelimit > 100) {
return res.status(429).redirect('/img/ratelimit.png'); return res.status(429).redirect('/img/ratelimit.png');
} }

@ -12,7 +12,7 @@ const express = require('express')
, server = require('http').createServer(app) , server = require('http').createServer(app)
, cookieParser = require('cookie-parser') , cookieParser = require('cookie-parser')
, configs = require(__dirname+'/configs/main.json') , configs = require(__dirname+'/configs/main.json')
, ipHash = require(__dirname+'/helpers/iphash.js') , processIp = require(__dirname+'/helpers/processip.js')
, referrerCheck = require(__dirname+'/helpers/referrercheck.js') , referrerCheck = require(__dirname+'/helpers/referrercheck.js')
, themes = require(__dirname+'/helpers/themes.js') , themes = require(__dirname+'/helpers/themes.js')
, Mongo = require(__dirname+'/db/db.js') , Mongo = require(__dirname+'/db/db.js')
@ -62,7 +62,7 @@ const express = require('express')
app.set('trust proxy', 1); app.set('trust proxy', 1);
//self explanatory middlewares //self explanatory middlewares
app.use(ipHash); app.use(processIp);
app.use(referrerCheck); app.use(referrerCheck);
// use pug view engine // use pug view engine

@ -19,6 +19,7 @@ block content
ul.mv-0 ul.mv-0
li: a(href='#whats-an-imageboard') What is an imageboard? li: a(href='#whats-an-imageboard') What is an imageboard?
li: a(href='/rules.html') What are the rules? li: a(href='/rules.html') What are the rules?
li: a(href='#site-operation') How do you run the site?
b Making posts b Making posts
ul.mv-0 ul.mv-0
//li: a(href='#how-to-post') How do I make a post? //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 owner: Same as board moderator
li Board moderator: All below, plus ban, delete-by-ip, sticky/sage/lock/cycle 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 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.

@ -38,6 +38,11 @@ details.toggle-label
label label
input.post-check(type='checkbox', name='global_ban' value='1') input.post-check(type='checkbox', name='global_ban' value='1')
| Global Ban Poster | 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 label
input.post-check(type='checkbox', name='no_appeal' value='1') input.post-check(type='checkbox', name='no_appeal' value='1')
| Non-appealable Ban | Non-appealable Ban

@ -3,31 +3,36 @@ details.toggle-label
.actions .actions
h4.no-m-p Actions: h4.no-m-p Actions:
label label
input.post-check(type='checkbox', name='delete' value='Delete post') input.post-check(type='checkbox', name='delete' value='1')
| Delete Posts | Delete Posts
label label
input.post-check(type='checkbox', name='delete_file' value='Delete files') input.post-check(type='checkbox', name='delete_file' value='1')
| Delete Files | Delete Files
label label
input.post-check(type='checkbox', name='spoiler' value='Spoiler files') input.post-check(type='checkbox', name='spoiler' value='1')
| Spoiler Files | Spoiler Files
label 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 | Delete from IP globally
label 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 | Dismiss Global Reports
label 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 | Global Ban Reporters
label 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 | Global Ban Poster
label 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 | Non-appealable Ban
label 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 | Show Post In Ban
label label
input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off')

@ -3,42 +3,47 @@ details.toggle-label
.actions .actions
h4.no-m-p Actions: h4.no-m-p Actions:
label label
input.post-check(type='checkbox', name='delete' value='Delete post') input.post-check(type='checkbox', name='delete' value='1')
| Delete Posts | Delete Posts
label label
input.post-check(type='checkbox', name='delete_file' value='Delete files') input.post-check(type='checkbox', name='delete_file' value='1')
| Delete Files | Delete Files
label label
input.post-check(type='checkbox', name='spoiler' value='Spoiler files') input.post-check(type='checkbox', name='spoiler' value='1')
| Spoiler Files | Spoiler Files
label label
input.post-check(type='checkbox', name='global_report' value='Global report') input.post-check(type='checkbox', name='global_report' value='1')
| Global Report | Global Report
label label
input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off') input#report(type='text', name='report_reason', placeholder='report reason' autocomplete='off')
label 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 | Delete from IP on board
label 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 | Delete from IP globally
label label
input.post-check(type='checkbox', name='dismiss' value='Dismiss reports') input.post-check(type='checkbox', name='dismiss' value='1')
| Dismiss Reports | Dismiss Reports
label label
input.post-check(type='checkbox', name='report_ban' value='Ban reporters') input.post-check(type='checkbox', name='report_ban' value='1')
| Ban Reporters | Ban Reporters
label label
input.post-check(type='checkbox', name='ban' value='Ban') input.post-check(type='checkbox', name='ban' value='1')
| Ban Poster | Ban Poster
label 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 | Global Ban Poster
label 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 | Non-appealable Ban
label 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 | Show Post In Ban
label label
input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') input(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off')

@ -37,3 +37,5 @@ mixin ban(ban, banpage)
textarea(rows=1 disabled='true') #{ban.appeal} textarea(rows=1 disabled='true') #{ban.appeal}
else if ban.allowAppeal else if ban.allowAppeal
| No appeal submitted | No appeal submitted
else
| -

Loading…
Cancel
Save