From 1b795c7b17a527edea1b52550e482dd8ee6b8c8d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 4 Dec 2019 19:07:04 -0700 Subject: [PATCH] Add basic flooding protection too --- src/protections/BasicFlooding.ts | 72 ++++++++++++++++++++++++++++++++ src/protections/protections.ts | 6 +++ 2 files changed, 78 insertions(+) create mode 100644 src/protections/BasicFlooding.ts diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts new file mode 100644 index 0000000..fc44ad5 --- /dev/null +++ b/src/protections/BasicFlooding.ts @@ -0,0 +1,72 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IProtection } from "./IProtection"; +import { Mjolnir } from "../Mjolnir"; +import { LogLevel, LogService } from "matrix-bot-sdk"; +import { logMessage } from "../LogProxy"; + +export const MAX_PER_MINUTE = 10; // if this is exceeded, we'll ban the user for spam and redact their messages +const TIMESTAMP_THRESHOLD = 30000; // 30s out of phase + +export class BasicFlooding implements IProtection { + + public lastEvents: { [roomId: string]: {[userId: string]: {originServerTs: number, eventId: string}[]} } = {}; + + constructor() { + } + + public get name(): string { + return 'BasicFloodingProtection'; + } + + public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise { + if (!this.lastEvents[roomId]) this.lastEvents[roomId] = {}; + + const forRoom = this.lastEvents[roomId]; + if (!forRoom[event['sender']]) forRoom[event['sender']] = []; + let forUser = forRoom[event['sender']]; + + if ((new Date()).getTime() - event['origin_server_ts'] > TIMESTAMP_THRESHOLD) { + LogService.warn("BasicFlooding", `${event['event_id']} is more than ${TIMESTAMP_THRESHOLD}ms out of phase - rewriting event time to be 'now'`); + event['origin_server_ts'] = (new Date()).getTime(); + } + + forUser.push({originServerTs: event['origin_server_ts'], eventId: event['event_id']}); + + // Do some math to see if the user is spamming + let messageCount = 0; + for (const prevEvent of forUser) { + if ((new Date()).getTime() - prevEvent.originServerTs > 60000) continue; // not important + messageCount++; + } + + if (messageCount >= MAX_PER_MINUTE) { + await logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`); + await mjolnir.client.banUser(event['sender'], roomId, "spam"); + // Redact all the things the user said too + for (const eventId of forUser.map(e => e.eventId)) { + await mjolnir.client.redactEvent(roomId, eventId, "spam"); + } + forUser = forRoom[event['sender']] = []; // reset the user's list + } + + // Trim the oldest messages off the user's history if it's getting large + if (forUser.length > MAX_PER_MINUTE*2) { + forUser.splice(0, forUser.length - (MAX_PER_MINUTE*2) - 1); + } + } +} diff --git a/src/protections/protections.ts b/src/protections/protections.ts index 89e2a31..894a221 100644 --- a/src/protections/protections.ts +++ b/src/protections/protections.ts @@ -16,6 +16,7 @@ limitations under the License. import { FirstMessageIsImage } from "./FirstMessageIsImage"; import { IProtection } from "./IProtection"; +import { BasicFlooding, MAX_PER_MINUTE } from "./BasicFlooding"; export const PROTECTIONS: PossibleProtections = { [new FirstMessageIsImage().name]: { @@ -23,6 +24,11 @@ export const PROTECTIONS: PossibleProtections = { "they'll be banned for spam. This does not publish the ban to any of your ban lists.", factory: () => new FirstMessageIsImage(), }, + [new BasicFlooding().name]: { + description: "If a user posts more than " + MAX_PER_MINUTE + " messages in 60s they'll be " + + "banned for spam. This does not publish the ban to any of your ban lists.", + factory: () => new BasicFlooding(), + } }; export interface PossibleProtections {