Fighting Bots & Spam with Feature Flags

Feature flags are one of those things where once they click, you see opportunities for them everywhere. From managing development or analytics tools across environments to things like protecting registration or contact forms, they come in handy in endless ways.

While nobody ever wants to take a registration or contact form offline, sometimes the bots and spammers leave us no choice. We can implement captchas, invisible captchas, email verification, manual verification, Akismet, and endless other solutions, but they'll still spray forms with garbage. When that happens, the last course of action is to just turn off the form entirely.

In most cases, that might require a re-deploy or a restart of the application, but with feature flags and Flipper, that registration form can be disabled and re-enabled again in real time with minimal effort. And with Flipper's responsive web views, you can make the change from a mobile device.

We've put together some basic Rails code to provide an example of another step teams can take to protect those public web forms. The simplest way to start is by creating a :registration flag in Flipper and making sure to enable it ahead of time. Since flags will default to disabled, we don't want to take registration offline unless we really need to. As an added bonus, we can leave registration disabled in staging unless we explicitly need to test it.

Screenshot of a 'registration' flag in Flipper Cloud where it's fully enabled in production and development but disabled in staging.
A 'registration' flag comes in handy because it's also easy to disable it for staging.

Updating the Registration Form View

The first and most obvious change to make in the code is disabling the registration form so that it's not visible, and if we do that, we should also display a message to let people know.

<% if Flipper.enabled?(:registration) %>
  # Show the Form
<% else %>
  # Hide the Form and Display a Message
<% end %>

But if we really want to minimize disruption, we should take into account that invited users who have an invite code are less likely to be problematic. So in those cases, we can go ahead and let them create an account.

<% if @signup.invitation.present? || Flipper.enabled?(:registration) %>
  # Show the Form
<% else %>
  # Hide the Form and Display a Message
<% end %>

So far, this is reasonably straightforward, but frequently the bots or spammers are posting directly to the endpoint. So in order to more robustly prevent junk registrations, we need some logic in the create action as well.

Blocking Direct Posts to the Create Action

For our create action, the logic can work just like it does within the form view. The only difference is that instead of proceeding, we redirect these attempts back to the sign up form where they can see the message that registration is temporarily disabled.

def create
  # ... Load the invitation

  unless invitation.present? || Flipper.enabled?(:registration)
    redirect_to :sign_up and return
  end

  # ... Do the normal things...
end

Expanding Test Coverage

With any significant change where a flag in the wrong state could be a problem, we want to update our tests as well. For that, we usually advise a test for each of the flag's states. For the registration page, we can check for the presence of our "unavailable" message or for the registration form.

test "(:registration disabled) GET sign_up_path renders message" do
  Flipper.disable(:registration)

  get signup_path
  assert_response :success
  assert_select '#unavailable'
end

test "(:registration enabled) GET sign_up_path displays form" do
  Flipper.enable(:registration)

  get signup_path
  assert_response :success
  assert_select 'form'
end

And in the case of our create action, we can do something similar but focus on where they end up after posting the registration values.

test "(:registration disabled) POST /sign_up does not create user" do
  Flipper.disable(:registration)

  assert_no_difference 'User.count' do
    post signup_path, params: {
      signup_form: {
        email: "test@example.com"
      }
    }
  end

  assert_redirected_to :sign_up
end

test "(:registration enabled) POST /sign_up creates user" do
  Flipper.enable(:registration)

  assert_difference 'User.count' do
    post signup_path, params: {
      signup_form: {
        email: "test@example.com",
      }
    }
  end

  assert_redirected_to :dashboard
end