Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Protection against mention flooding #210

Closed
wants to merge 21 commits into from

Conversation

MTRNord
Copy link

@MTRNord MTRNord commented Feb 6, 2022

This basically tries to solve the current way of spam.

This is an adapted wordfilter protection. At the time of writing, this is not tested anywhere.

It only matches mxid based mentions currently. Not other mentions per default push rules.

TODO

  • Finish the warning branch
  • Test it (I have a docker of this branch at coreharbor.kubernetes.midnightthoughts.space/matrixdotorg/mjolnir:mentionflood-protection )

image
image

Signed-off-by: Marcel Radzio <mtrnord[at]nordgedanken.dev>

@Yoric Yoric self-requested a review February 6, 2022 09:21
Copy link
Contributor

@Yoric Yoric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this PR!

src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
@MTRNord
Copy link
Author

MTRNord commented Feb 6, 2022

Seems like my vscode likes to reformat stuff differently somehow :/ I can revert the unrelated formatting changes if needed.

@Yoric Yoric requested a review from jesopo February 6, 2022 12:33
@MTRNord MTRNord force-pushed the MTRNord/mention-flood branch from a0e913d to b7bdb05 Compare February 7, 2022 14:01
@Yoric
Copy link
Contributor

Yoric commented Feb 14, 2022

Oops, looks like CI was waiting for approval before launching.

Let's see how it goes.

Copy link
Contributor

@jesopo jesopo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

big thank for the PR

src/protections/ProtectionSettings.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
@MTRNord MTRNord requested a review from jesopo February 22, 2022 20:32
@cremesk
Copy link
Contributor

cremesk commented Mar 1, 2022

can we merge it please? i really need this protection :D

@MTRNord MTRNord requested a review from Yoric March 3, 2022 14:52
@Yoric
Copy link
Contributor

Yoric commented Mar 7, 2022

@MTRNord Just back from trip, I'll try and review this today or tomorrow.

Copy link
Contributor

@Yoric Yoric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR looks good, could you add a test?

src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Show resolved Hide resolved
src/protections/MentionFlood.ts Outdated Show resolved Hide resolved
src/protections/MentionFlood.ts Show resolved Hide resolved
src/protections/ProtectionSettings.ts Outdated Show resolved Hide resolved
@Yoric
Copy link
Contributor

Yoric commented Mar 8, 2022

@MTRNord I realize I wasn't clear: when I write "test", I mean a new integration test, see e.g. test/integration.

@MTRNord MTRNord force-pushed the MTRNord/mention-flood branch from d337ff4 to 219ecce Compare March 8, 2022 13:44
@Yoric
Copy link
Contributor

Yoric commented Apr 7, 2022

@MTRNord What's the status of this PR?

@MTRNord
Copy link
Author

MTRNord commented Apr 7, 2022

@MTRNord What's the status of this PR?

Sorry, I nearly have the tests done, but there is some small thing I still need to do. Just didn't get to it yet due to other work related tasks that took higher priority. Want to do the rest of the code on the weekend.

@Yoric
Copy link
Contributor

Yoric commented Apr 7, 2022

Don't hesitate to yell if you need help!

@MTRNord MTRNord requested a review from Yoric May 1, 2022 12:33
Copy link
Contributor

@Gnuxie Gnuxie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some observations about the test, you don't need to act on them but they're just things that would keep them cleaner this or next time

Comment on lines +78 to +81
} finally {
await moderator.stop();
await userA.stop();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't matter too much but since you stop these clients in the afterEach hook you should be able to take out these try/finally blocks

const userA = await newTestUser({ name: { contains: "a" } });

this.moderator = moderator;
this.userA = userA;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick but userA doesn't tell us what this use is doing. they could unrelated to moderation of the room, they could just be a participant, they could even be a user who is going to be spamming, who is userA?

Comment on lines +1 to +85
import { MatrixClient } from "matrix-bot-sdk";

/**
* Returns a promise that resolves to the first event while sending events.
* @param client A MatrixClient that is already in the targetRoom. We will use it to listen for the event produced by targetEventThunk.
* This function assumes that the start() has already been called on the client.
* @param targetRoom The room to listen for the reply in.
* @param produceEvents A function that produces an events when called.
* @returns The first event.
*/
export async function getFirstMessage(client: MatrixClient, targetRoom: string, produceEvents: () => Promise<any>): Promise<any> {
let reactionEvents = [];
const addEvent = function (roomId, event) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.room.message') return;
reactionEvents.push(event);
};
let targetCb;
try {
client.on('room.event', addEvent);
await produceEvents();
for (let event of reactionEvents) {
return event;
}
return await new Promise(resolve => {
targetCb = function (roomId, event) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.room.message') return;
resolve(event);
};
client.on('room.event', targetCb);
});
} finally {
client.removeListener('room.event', addEvent);
if (targetCb) {
client.removeListener('room.event', targetCb);
}
}
}

/**
* Returns true if the user got changed to the wanted membership type.
* @param client A MatrixClient that is already in the targetRoom. We will use it to listen for the event produced by targetEventThunk.
* This function assumes that the start() has already been called on the client.
* @param targetRoom The room to listen for the reply in.
* @param userId The user that should get checked for.
* @param membership The membership type that the user should get.
* @returns The first event.
*/
export async function checkMembershipChange(client: MatrixClient, targetRoom: string, userId: string, membership: string): Promise<boolean> {
let membershipEvents = [];
const addEvent = function (roomId, event) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.room.member') return;
membershipEvents.push(event);
};
let targetCb;
try {
client.on('room.event', addEvent);
for (let event of membershipEvents) {
if (event.state_key == userId && event.content.membership == membership) {
return true;
} else if (event.state_key == userId && event.content.membership != membership) {
return false;
}
}
return await new Promise(resolve => {
targetCb = function (roomId, event) {
if (roomId !== targetRoom) return;
if (event.type !== 'm.room.member') return;
if (event.state_key == userId && event.content.membership == membership) {
resolve(true);
} else if (event.state_key == userId && event.content.membership != membership) {
resolve(false);
}
};
client.on('room.event', targetCb);
});
} finally {
client.removeListener('room.event', addEvent);
if (targetCb) {
client.removeListener('room.event', targetCb);
}
}
}
Copy link
Contributor

@Gnuxie Gnuxie May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's not your fault/related to this PR necessarily, but i am a little worried now about how this pattern is being used everywhere https://github.com/MTRNord/mjolnir/blob/MTRNord/mention-flood/test/integration/commands/commandUtils.ts#L5-L97

@Yoric
Copy link
Contributor

Yoric commented May 9, 2022

Sorry for the delay, I'm just back from vacation, I'll try and review this today.

Copy link
Contributor

@Yoric Yoric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite good!
You'll need a few more changes, but we're getting there.

const DEFAULT_ACTION = "ban";

const LOCALPART_REGEX = "[0-9a-z-.=_/]+";
// https://github.com/johno/domain-regex/blob/8a6984c8fa1fe8481a4b99be0fa7f2a01ee17517/index.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could you clarify this comment?

e.g.

/**
 * A regex to extract a domain name.
 * Based on https:// ...
 */

Same things below.

const LOCALPART_REGEX = "[0-9a-z-.=_/]+";
// https://github.com/johno/domain-regex/blob/8a6984c8fa1fe8481a4b99be0fa7f2a01ee17517/index.js
const DOMAIN_REGEX = "(\\b((?=[a-z0-9-]{1,63}\\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,63}\\b)";
// https://stackoverflow.com/a/5284410
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: one line between the various const would help with readability.

import { htmlEscape } from "../utils";
import { BooleanProtectionSetting, DurationMSProtectionSetting, NumberProtectionSetting, OptionListProtectionSetting } from "./ProtectionSettings";

const DEFAULT_MINUTES_BEFORE_TRUSTING = 20 * 60 * 1000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Doesn't look like minutes to me.


settings = {
// Time in which this protection takes action on users
minutesBeforeTrusting: new DurationMSProtectionSetting(DEFAULT_MINUTES_BEFORE_TRUSTING),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely not minutes.

if (minsBeforeTrusting > 0) {
const joinTime = mjolnir.roomJoins.getUserJoin({ roomId: roomId, userId: event['sender'] });
// If we know the user and have its time we check if.
// Otherwise we assume a bug and still mark them as suspect just to make sure.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that everybody who joined before the start of Mjölnir is considered suspect. I believe that you should rather take the opposite policy and assume that if we don't have a joinTime, the user has been there for a long time.

if (event['type'] === 'm.room.message') {
const message: string = content['formatted_body'] || content['body'] || "";

// Check conditions first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could you mention that we check conditions first because it's fast?

const content = event['content'] || {};
const minsBeforeTrusting = this.settings.minutesBeforeTrusting.value;

if (event['type'] === 'm.room.message') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it would be a little more readable if you did the opposite, e.g.

if (event['type'] !== 'm.room.message') {
  return;
}

return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir enable MentionFlood` });
});
} finally {
await moderator.stop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, there are cases in which this finally will not be executed. In particular, in case of timeout, or if the call to joinRoom fails for some reason. This causes massive breakages in our test suite.

Instead of this finally, you need to declare the moderator as:

this.moderator = await newTester(...)

And add

    afterEach(async function() {
        await this.moderator?.stop();
        // also, any other client, e.g. `userA`, `userB`
    });

towards the beginning or end of this describe.

// Create users
const mjolnir = config.RUNTIME.client!;
const mjolnirUserId = await mjolnir.getUserId();
const moderator = await newTestUser({ name: { contains: "moderator" } });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, too, turn this into this.moderator = ... and get rid of the finally.

client.removeListener('room.event', targetCb);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing newline.

@MTRNord MTRNord closed this Jun 23, 2022
@MTRNord MTRNord deleted the MTRNord/mention-flood branch June 23, 2022 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants