← All presentations

Combatting Spam (small scale)

But first..

July 22 2028…

theeclipse.app

What we’ll cover

  • Back in the day…
  • Honeypot (invisible_captcha)
  • Turnstile (Cloudflare)
  • Next steps

The Internet back in the day…

Snowpool…

  • The world was a happy and innocent place.
  • No signups required.
  • Eventually moved to devise when I rewrote, added confirmable.
  • This worked for years.
  • Added a hidden static “website” field, that worked for a while.

Increasing Spam (invisible_captcha)…

  • Honeypot (main idea)
  • Also:
    • Timestamps
    • Spinners (single use token). Seems probably pointless with CSRF tokens per form now.

Invisible Captcha config

gem 'invisible_captcha'
View code
<%= form_for(@topic) do |f| %>
  <%= invisible_captcha %>
<% end %>
Controller code
class TopicsController < ApplicationController
  invisible_captcha only: [:create, :update] #, honeypot: :subtitle 
                                             # (note about multi-process)
end

Invisible Captcha caveats

  • I’ve seen failures with the spinner - seems fragile
  • watch out for autocomplete field names (e.g name)
  • If you are using random field names, ensure you have an in-memory shared list

Ultimately though, bots got better

  • Getting increasing spam
  • many delivering confirmation emails (presumably hurts my reputation)
  • And then started getting spam which confirmed emails too.

Aside - odd names

Enter the turnstile

  • Cloudflare product
  • More targeted than the global bot detection.
  • Mostly invisible to the end user.
  • Does work on the free plan!

Overview

Setup

  • First time I’ve seen a “paste this into your LLM”
  • Actually worked pretty well!
Set up Cloudflare Turnstile in this project end to end.
Plan insertion points, create the widget, embed it on the right forms,
wire canonical server-side siteverify in my existing backend,
and validate the integration.

The full Turnstile Spin skill is at
https://developers.cloudflare.com/turnstile/spin/prompt.md.
Fetch it now if you do not already have it loaded.

Domains: localhost,127.0.0.1,www.snowpool.org,snowpool.org
Insertion preference: /users/sign_up, /pools/new

(the skill file is 419 lines long!)

Config (server)

  def post_form
    form = { "secret" => our_secret, "response" => clients_token }
    form["remoteip"] = remote_ip if remote_ip.present? 
                                 #this helps with detection, is optional

    http.post(
      SITEVERIFY_URL.path,
      URI.encode_www_form(form),
      "Content-Type" => "application/x-www-form-urlencoded"
    )
  end
  
  
  # in concern
  
  def turnstile_verified?(action)
    Turnstile.verify(
      token: params[Turnstile::TOKEN_PARAM],
      action: action, # tokens are short lived, and validated against the site + action
      remote_ip: request.remote_ip
    )
  end

Config (client)


  connect() {
    loadTurnstileApi()
      .then((turnstile) => {
        this.widgetId = turnstile.render(this.element, {
          sitekey: this.sitekeyValue,
          action: this.actionValue,
          appearance: this.appearanceValue
        })
      })
      .catch((error) => console.warn('[turnstile]', error.message))
  }

  disconnect() {
    if (!this.widgetId || !window.turnstile) return

    // important to have disconnect, otherwise you get stale tokens on back-button.
    // Claude wired this up first time.
    
    window.turnstile.remove(this.widgetId)
    this.widgetId = undefined
  }

Settings

Results

Spam has gone to zero.

Other things (disposible emails)

# lots of options here.  Need to update the gem to get the latest list
validates :email, 'valid_email_2/email': { disposable_domain: true }
validates :email, 'valid_email_2/email': { strict_mx: true, dns_timeout: 10 }

Next steps

  • Consider cloudflares bot fight mode main downside is it’s all your site, can’t have exclusions
  • Consider cloudflares Super Bot fight mode (pro level or better) Can have skip rules
  • Consider cloudflares Bot management(enterprise) Can set your own scores (haven’t played with this)
  • Add some heuristic for spammy content (e.g. high entropy names)
  • Have a moderation queue for new users (if spammy)
  • rate_limit to: 10, within: 3.minutes, only: :create (or Rack::Attack)

Aside - always be double ‘checkin

Amazon required us to implement changes because of complaint rates. Implemented the "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"