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,
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]
}

@ -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

@ -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({

@ -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_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

@ -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,

@ -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;

@ -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 = {

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

@ -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

@ -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.

@ -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

@ -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')

@ -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')

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

Loading…
Cancel
Save