How to identify a bot behaviour on a click?

432 Views Asked by At

I've developed a simple app with a single button that triggers different text changes. My aim is to distinguish between human users and potential bots. If the clicks happen too rapidly, it's likely a bot, and that's when I'd want to present a ReCaptcha.

My approach involves monitoring the time intervals from the first button click and the subsequent ones over a 5-minute window. If the clicking maintains an extremely consistent speed, say, a click every 10 milliseconds, it's a red flag for potential bot activity. Humans typically can't sustain such robotic precision, whether the interval is 10 milliseconds, 1 second, or 2 seconds.

Now, what I'm confused about is, is this method correct? And if so, what could be done (in code)?

Here is a Screenshot.

frameLayoutBtn.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    v.startAnimation(animation);

    ClickCounter++;

    catStr = getCatTextFromDatabase(ClickCounter);
    if (ClickCounter > 1000) {
      CountText.setText("Count:" + ClickCounter);
    }

    if (catStr != null && !catStr.isEmpty()) {
      CatText.setText(catStr);
    }
  }

  private String getCatTextFromDatabase(long counter) {
    DatabaseHelper dbHelper = new DatabaseHelper(getApplicationContext());
    return dbHelper.getTextForCounter(counter);
  }
});
2

There are 2 best solutions below

3
On
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private Button button;
    private long lastClickTime = 0; // Initialize the time of the last click
    private final long maxInterval = 100; // Set the maximum allowed interval in milliseconds

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                long currentTime = System.currentTimeMillis();
                long clickInterval = currentTime - lastClickTime;
                
                if (clickInterval < maxInterval) {
                    // Handle the case of a potentially automated click (e.g., show CAPTCHA)
                    // You can implement your CAPTCHA logic here.
                } else {
                    // Handle the normal user interaction
                }
                
                lastClickTime = currentTime;
            }
        });
    }
}

In this code:

We use a lastClickTime variable to store the time of the last click.

The maxInterval variable represents the maximum allowed time interval between clicks. In this example, it's set to 100 milliseconds, but you can adjust it according to your needs.

When the button is clicked, the code calculates the time interval since the last click and checks if it's less than the maxInterval. If the interval is shorter than the allowed maximum, you can implement your CAPTCHA logic or any other action to deal with potentially automated behavior.

4
On

I have never actually used something like this in a productive environment, but I would suggest two things:

  1. Log the last n click times (or times between clicks) in a Collection or helper class that at the point of adding automatically disposes of older, now irrelevant entries. I like java.util.Stack in helper classes with similar purposes.
  2. Don't rely on exact numbers. I have encountered bots that randomize their output events to prevent exactly that. You should use a combination of something like mean click time (to idenfity unrealistically fast clicks) and standard deviation (to identify intervals that are too regular)

Edit:

Maybe something like this, although I couldn't be bothered to implement standard deviation or similar. Feel free to replace methodology within valid() with any given statistics or replace the Stack with a collection of your choosing. You would hvae to replace the pop() with remove(size()-1) though.

As it stands, this would return false if you add() a time between clicks that is unrealistically small (as defined in the constructor) and returns the assessment of it being a bot with valid() periodically.

It would also be smart to not recalculate min mean an max like that because that takes time.

import java.util.Stack;

public class Analyser extends Stack<Long> {

    int maxsize;
    long minIntervalInMs;
    float percentRange;

    /**
     *
     * @param maxsize how many data points should be stored
     * @param minIntervalInMs what is the minimum valid input interval, e.g. 100
     * for 100 ms
     * @param percentRange [0.0f - 1.0f] what deviation from mean is probably
     * valid? e.g. 0.05f means that +- 5% around the mean of all values is valid
     */
    public Analyser(int maxsize, long minIntervalInMs, float percentRange) {
        super();
        this.maxsize = maxsize;
        this.minIntervalInMs = minIntervalInMs;
        this.percentRange = percentRange;
    }

    /**
     *
     * @param timeSinceLastAction
     * @return is the input time valid, i.e. longer than the minIntervalInMs
     */
    @Override
    public boolean add(Long timeSinceLastAction) {
        add(0, timeSinceLastAction);
        if (size() > maxsize) {
            pop();
        }
        return timeSinceLastAction >= minIntervalInMs;
    }

    /**
     *
     * @return under given parameters, is the input valid (returns true when
     * not enough data available)
     */
    public boolean valid() {
        if (size() < maxsize) {
            return true;
        }
        float min = Float.POSITIVE_INFINITY;
        float max = Float.NEGATIVE_INFINITY;
        long mean = 0;
        for (int i = 0; i < size(); i++) {
            long v = get(i);
            min = Math.min(v, min);
            max = Math.max(v, max);
            mean += v;
        }
        mean /= size();
        float v_min = min / mean;
        float v_max = max / mean;
        return v_min <= 1 - percentRange && v_max >= 1 + percentRange;
    }

    public static void main(String[] args) {
        Analyser analyser = new Analyser(100000, 1000, .05f);
        for (int i = 0; i < 100000; i++) {
            analyser.add((long) (1000 + 100 * Math.random()));
        }
        System.out.println(analyser.valid());
    }
}