Skip to content

Spam protection tools - #2138

Open
ArtOfCode- wants to merge 24 commits into
developfrom
art/spam-tools
Open

Spam protection tools#2138
ArtOfCode- wants to merge 24 commits into
developfrom
art/spam-tools

Conversation

@ArtOfCode-

@ArtOfCode- ArtOfCode- commented Aug 14, 2026

Copy link
Copy Markdown
Member
  • Automatically add spammers to STAT on deletion (with exceptions so we don't automatically block every deleted account).
  • IP (v6?) CIDR range flagging?
  • Identical post check: block posting a post identical to any of your previous posts for users with <10 non-deleted posts.
  • Potential spammer block: users with <10 non-deleted posts are blocked from posting if there's an active or helpful spam flag against any of their posts.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.70330% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.31%. Comparing base (82bc536) to head (5fff126).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
app/controllers/categories_controller.rb 66.66% 1 Missing ⚠️
app/controllers/users/registrations_controller.rb 66.66% 1 Missing ⚠️
app/models/concerns/post_creation_validations.rb 97.56% 1 Missing ⚠️
Additional details and impacted files
Components Coverage Δ
controllers 76.96% <83.33%> (-0.03%) ⬇️
helpers 85.47% <ø> (ø)
jobs 79.93% <100.00%> (+2.49%) ⬆️
models 93.42% <97.87%> (+0.16%) ⬆️
tasks 61.11% <ø> (ø)
scripts ∅ <ø> (∅)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

def post_types
@post_types = @category.top_level_post_types
if @post_types.one?
if !user_signed_in?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Apparently one of our system tests was relying on this redirecting, which was order-dependent, so I've moved it to always redirect when not signed in.

@@ -0,0 +1,34 @@
module UserTestHelpers

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This whole file is just the existing helpers extracted from users_controller_test.rb to allow for a restructure.

@@ -0,0 +1,252 @@
require 'test_helper'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This file is the existing tests relating to user mod tools extracted into their own file, because the users_controller_test.rb was getting excessively long.

@ArtOfCode-
ArtOfCode- requested review from a team and cellio August 16, 2026 15:24
@ArtOfCode-
ArtOfCode- marked this pull request as ready for review August 16, 2026 15:24
@cellio

cellio commented Aug 16, 2026

Copy link
Copy Markdown
Member

A couple initial questions (haven't started testing/reviewing yet):

  • How and when does the user find out about the block on identical posts? (I don't see any view changes in this PR.)
  • Identical posts across the network, right?
  • For the potential spammer block, what feedback does the user get? If a legit user got caught in it, would there be a clear path to appeal the block?
  • We have a bug where sometimes state doesn't get cleared when a post is made, and it still shows as you having a pending draft. Do we have to worry about interactions with that here, or is that a separate issue? (Want to make sure that wouldn't count as a dupe.) I've never been able to reproduce that bug in my dev environment, so I don't know how to test it.

@ArtOfCode-

ArtOfCode- commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

How and when does the user find out about the block on identical posts? (I don't see any view changes in this PR.)

They don't find out specifically what blocked the post - in line with the obscurity in the rest of our spam tools, we spit out a "useful" error message and leave it at that. I don't see many (any?) legitimate reasons for a new user to post identical post content either multiple times on one site, or several times across the network, so I'm reasonably comfortable with that.

There's more of a chance of an established user/mod/admin posting identical posts across the network (I think we've done it as staff), but anyone with >=10 (configurable) non-deleted posts network-wide is exempt from this completely.

Identical posts across the network, right?

Yes.

For the potential spammer block, what feedback does the user get? If a legit user got caught in it, would there be a clear path to appeal the block?

Same feedback - a "useful" error message. They also wouldn't be able to post on Meta so would have to contact support for help. However:

  • Helpful spam flags only block posting for 7 (configurable) days, after which they're considered expired for this purpose, for exactly this reason. 7 days might be too long - 1 would probably be enough, I just wanted a period where the flag could be marked as helpful and not drop the blocking effect, because...
  • Active i.e. unreviewed spam flags block posting indefinitely, but only until they're reviewed.

The theory is that a potential spammer would be blocked from posting until a moderator reviews the spam flag. On review, they'd then mark the flag as helpful and nuke the user, obviating the need for a post block, and the new deletion job will apply a STAT block. A legitimate user who was wrongly spam-flagged would also be blocked until review, but would then be released once the flag is declined.

Likewise, users with >=10 (configurable) non-deleted posts network-wide are completely exempt.

We have a bug where sometimes state doesn't get cleared when a post is made, and it still shows as you having a pending draft. Do we have to worry about interactions with that here, or is that a separate issue?

Pending drafts aren't saved as posts (they're actually not in the DB at all, but in Redis), so won't interact with this at all unless the user tries to post it a second time without modification - which would be rejected.

@cellio

cellio commented Aug 17, 2026

Copy link
Copy Markdown
Member

That all sounds good to me. Thanks for explaining!

@cellio

cellio commented Aug 17, 2026

Copy link
Copy Markdown
Member

Duplicate posts:

I tested with a user who had exactly 10 posts, 3 of them deleted. I copied the content of an existing post and was able to repost it. (Tried with both questions and answers.)

I then tried with a user with no posts -- added an answer (ok), then cut/pasted that on a different question and got the error as expected. Does this mean we're only checking against posts created after this change is in place? (That's fine, but I want to check that it's intentional and not a bug.)

I tested duplicate posts across communities; worked as expected. Adding a carriage return was enough to make the posts non-identical (post succeeded); do we want to normalize or are we not worried about this case?

--

Spam flags:

Tested with a user with < 10 posts and an active spam flag, both local community and elsewhere on the network. Both produced errors as expected, and declining the flag enabled posting again. Note: I tested this with the same user from my first paragraph: the flag block worked but the duplicate content block didn't. Are we accidentally counting deleted posts in one place but not the other?

Tested with a user with > 10 posts and an active spam flag. Posting permitted, as expected.

--

Error message:

The actual error message looks like one of our 418s -- "The heisenbug vanished when we opened the logs. Please look away and try again." I hadn't followed your "useful" link before; while your comment seemed fine when I read it, seeing the cryptic error on the post page "live" feels different. I'd be more comfortable with a vague error than a cryptic one, especially in the case of spam flags because sometimes people are wrong about that. Can we just say something like "post rejected"? A real person would then come to meta to ask, and meanwhile a spammer doesn't get useful information.

--

Deleted spammers:

I'm a little afraid to IP-block myself on my dev environment, so I haven't tested deleting spammers.

Comment thread app/jobs/delete_user_job.rb
Comment thread app/models/concerns/post_creation_validations.rb
Comment thread app/models/user.rb
Comment thread config/config/server_settings.yml
@trichoplax

Copy link
Copy Markdown
Contributor

Pending drafts aren't saved as posts (they're actually not in the DB at all, but in Redis), so won't interact with this at all unless the user tries to post it a second time without modification - which would be rejected.

Does "rejected" here mean that an accidental second posting will not work, leaving the user unharmed, or that they will be flagged and blocked from posting anything?

Some users on seeing their draft question or answer still present after posting (due to #1545) assume that posting failed, and post again. Previously this has led to them noticing the unintended duplicate soon after and raising it on Meta (or commenting under an existing Meta post). Will this still be possible with this pull request or will such a user be blocked from posting on Meta?

I can imagine a new user caught by this might not realise that emailing support is even an option.

@ArtOfCode-

Copy link
Copy Markdown
Member Author

Does "rejected" here mean that an accidental second posting will not work, leaving the user unharmed, or that they will be flagged and blocked from posting anything?

@trichoplax the former - the identical post check just blocks that post, and doesn't apply any ongoing consequences to the account.

@ArtOfCode-

Copy link
Copy Markdown
Member Author

@cellio

I tested with a user who had exactly 10 posts, 3 of them deleted. I copied the content of an existing post and was able to repost it. (Tried with both questions and answers.)

No repro - I have a user with 10 posts, 2 deleted. I copied a post's Markdown and got the expected error. Did you copy the Markdown or the post's rendered appearance? The latter might have introduced changes in the Markdown, which would mean the post passes the check.

I'd be more comfortable with a vague error than a cryptic one, especially in the case of spam flags because sometimes people are wrong about that.

Done.

I'm a little afraid to IP-block myself on my dev environment, so I haven't tested deleting spammers.

If you want to test it, you can always go into the DB manually afterwards to unblock yourself - DELETE FROM blocked_items;

@cellio

cellio commented Aug 17, 2026

Copy link
Copy Markdown
Member

No repro - I have a user with 10 posts, 2 deleted. I copied a post's Markdown and got the expected error. Did you copy the Markdown or the post's rendered appearance? The latter might have introduced changes in the Markdown, which would mean the post passes the check.

I did copy the markdown (went into edit to get it), but now I can't repro either with the same user and same post -- maybe I missed a terminal newline and that made the difference?

I'd be more comfortable with a vague error than a cryptic one, especially in the case of spam flags because sometimes people are wrong about that.

Done.

Thank you.

Comment thread app/models/concerns/post_creation_validations.rb

@cellio cellio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Someone else should review code (particularly the queries and CIDR). I didn't test CIDR/auto-escalated-flag.

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.

3 participants