How to Use Fingerprint to Prevent Bot Attacks

Prevent bot attacks

The purpose of this tutorial is to demonstrate how easy it is to detect and mitigate bad bot account signups with Fingerprint Pro. Users will first create a sample registration website with basic security controls to prevent multiple signups, then simulate a bot attack to defeat those controls using open source tools. Lastly, they will integrate Fingerprint Pro into the website’s security workflow to detect/stop advanced bots, even when other security methods have failed.

As a starting point, readers may wish to (re)visit the tutorial on How to Prevent Multiple Signups With Fingerprint Pro, which covers more basic attempts at multiple account creation (e.g., revisiting the registration form in incognito mode).

A World of Bad Bots

With a large percentage of today’s web traffic created by bad bots, website owners are increasingly falling victim to automated attacks. Bat bots accounted for a quarter (25.6%) of all internet traffic in 2020, up from 24.1% in 2019. Suffice to say, stronger security controls are required to survive in this hostile cyber landscape.

And who would know better than the operator of the most trafficked website in the world? Earlier this year, a massive bot attack targeted Google Analytics (GA) with hordes of bogus traffic sent to non-existent web pages, skewing report metrics for many of its customers. Despite implementing controls to mitigate the zombie traffic, Google’s efforts are only stopgap measures in a perpetual game of whack-a-mole against increasingly sophisticated opponents.

According to Imperva's 2021 Bad Bot Report, bad bots are responsible for an ever-widening array of malicious activity, including:

  • Scraping: allows perpetrators to harvest data from a website without the owner’s permission. Common methods include content scraping (i.e., stealing proprietary content) and price scraping (i.e., extracting pricing information for competitive purposes)
  • Gift Card Balance Checking: allows malicious attackers to steal money from active gift card accounts
  • Denial of Service: slows a website’s performance, causing downtime or a degraded visitor experience.
  • Account Creation: allows attackers to create free accounts for creating spam and/or amplifying propaganda.

Bot attack prevention should be a shared concern amongst all website owners, whether it’s for preserving the accuracy of website traffic analytics or ensuring the safe operations of vaccine booking websites. Unfortunately, malicious actors continue refining their bots’ ability to mimic human behavior, making the goal of detection a constantly moving target.

Fingerprint Pro and Bot Detection

This tutorial uses Fingerprint Pro to prevent bad bots from defeating an account signup form’s email verification system. As the most advanced browser fingerprinting solution on the market, Fingerprint Pro can identify unique website visitors with an accuracy of 99.5%.

It’s worth noting that while Fingerprint Pro is highly effective at detecting fraudulent activity and abuse patterns typical of bots (e.g., repeat visits and multiple form submissions), it doesn’t explicitly differentiate between bot and human visitors. If you want to flag visitors based on their likelihood of being a bot, we have built an open source bot detection library to recognize bots through the detection of automation tools, browser spoofing, and virtual machines. This free library can be used in conjunction with Fingerprint Pro to both detect bots and generate a persistent visitor identifier.

Tutorial Requirements

  • Fingerprint Pro is required; new users can sign up for a 14-day free trial.
  • Splinter is used for simulating a user (or many users) completing the signup form. The popular Python-based testing framework enables the automation of browser actions like visiting URLs and interacting with web page elements, among others.
  • 1secMAIL is used to generate disposable mailboxes during the account creation process.

Lastly, the following items should be installed in the environment where the tutorial files are located:

  • Yarn - for installing the multiple signup demo project dependencies
  • Pip - for installing Splinter
  • Node.js - provides the JavaScript runtime environment and web server
  • Docker/PostgreSQL - for quickly spinning up a transient PostgreSQL instance

Preparing the Signup Form and Database

This tutorial involves a fictional online course platform that offers free 14-day trials. The website owner wants to prevent trial users from registering several accounts with multiple emails and extending their trials indefinitely.

Start with the existing sample registration form provided by Fingerprint called multiple-signup-demo.

Run git checkout against the initial-project-setup branch:

git clone -b initial-project-setup \
     https://github.com/fingerprintjs/fingerprintjs-multiple-signups-example.git \
     signup_form
cd signup_form

Next, make sure all the project dependencies are correctly installed:

yarn install

A PostgreSQL database is required for storing user registrations. To save time, use Docker to create a transient PostgreSQL server:

docker run --name sqlserver -e POSTGRES_PASSWORD=my-secret-pw  -p 5433:5432 -d postgres

Once the database has been created, run the following commands to generate the initial schema:

psql --username=postgres --port=5433 --password
postgres=#  create schema development;

Run the initial migration inside the sql/ folder:

psql --username=postgres --port=5433 --password development < sql/0001_initial.sql

Start the application to verify that the registration form is working correctly:

yarn start

Open localhost:3002/signup in a browser. If everything is working correctly, the signup form should appear as follows:

Registration form

Insert a dummy email address and submit the form:

A page is displayed confirming that the account was created successfully.

Start the registration process again with an empty form. Insert the same email address as before and submit the form—it should return the error User with this email already exists:

Email taken already

The following is the signup form’s index.js code:

// signup form submission
  app.post('/signup', async function signup(req, res, next) {
    const {email} = req.body

    try {
      if (!email) {
        throw new Error('email is required')
      }

      const result = await client.query('insert into users(email) values($1) returning *', [email])
      console.log(`${result.rows[0].email} added to the db`)

      res.render('signup_success', {layout: 'index'})
    } catch (e) {
      console.error(e)

      let message = e.message

      if (e.code === '23505') {
        message = 'User with this email already exists'
      }

      res.render('signup', {layout: 'index', error: message, email})
    }
  })

After the user submits the form, the system will attempt to create a new record in the users table. If the email already exists, the error is caught and the user is notified about the duplicate entry.

Simulating an Attack with Splinter

Now that the signup form is up-and-running, the next step is to use Splinter and some Python code to simulate an attacker’s actions. Fingerprint Pro has yet to be installed, so using unique email addresses should be enough to automate multiple account signups.

Start by creating a new directory called signup_bot:

mkdir signup_bot 
cd signup_bot

Next, install Splinter by running the following:

pip install splinter

Verify that Splinter is working by copying the following code and saving it as main.py:

from splinter import Browser
import random
import string

with Browser() as browser:
    # Visit URL
    url = "http://localhost:3002/signup"
    browser.visit(url)

    # Fill the form
    letters = string.ascii_lowercase
    email = ''.join(random.choice(letters) for i in range(10))
    email = "{}@thebuilder.com".format(email)
    browser.fill('email', email)

    # Submit the form
    button = browser.find_by_text('Create Account')
    button.click()

    # Validate form submission
    if browser.is_text_present('We have sent you a link to confirm your account'):
        print("[SUCCESS] Registration Successful! :)")
    else:
        print("[ERROR] Registration Failed :(")

This is a simple version of the code that will:

  • open a browser instance
  • load the target URL
  • fill the email field with a randomly generated email
  • submit the form and validate the appearance of the email confirmation page

Run the script:

python main.py

The following message should log out into the terminal: [SUCCESS] Registration Successful! :)

Modern Security Measures (And How To Defeat Them)

At this point, the registration process involving the signup form submission has been successfully automated, as our demo signup form does not require any additional authentication steps. However, since live signup processes will have additional steps to discourage bot activity, the next section will include an optional demonstration on how to circumvent one of the most common security measures with automation. If you’d like, you can skip this section.

One common security workflow is to only activate new accounts after a user clicks on a link sent via email. This added step can stop randomly generated email addresses from being able to complete signup if the intent is to create valid and active accounts.

1sec MAIL is a service that generates disposable mailboxes on demand; conveniently, a Python library is also available for use in this tutorial.

Install the library using the following command:

pip install onesecmail

The library can be used directly from the Python console:

python
>>> from onesecmail import OneSecMail 
>>> mailbox = OneSecMail.get_random_mailbox() # Generates a random mailbox
>>> print(mailbox)
<OneSecMail [vbbew64ng2bq@1secmail.com]>
>>> messages = mailbox.get_messages()
>>> print(messages)
[<EmailMessage; from='registration@localhost', subject='Verify your email', date='2021-06-28 00:03:27+02:00'>]

Previously, the script was auto-generating email addresses randomly without actually creating real accounts/mailboxes—any confirmation emails sent to these addresses would therefore bounce. By integrating 1secMail, the signup bot can use real email accounts to capture the validation links.

Update the signup bot script to use 1secMAIL:

from splinter import Browser
from onesecmail import OneSecMail


def create_mailbox():
    mailbox = OneSecMail.get_random_mailbox()
    return mailbox

def registration(email):
    with Browser() as browser:

        # Visit URL
        url = "http://localhost:3002/signup"
        browser.visit(url)

        # Fill the form
        browser.fill('email', email)

        # Submit the form
        button = browser.find_by_text('Create Account')
        button.click()

        # Validate form submission
        if browser.is_text_present('We have sent you a link to confirm your account'):
            print("[SUCCESS] Registration Successful! :) - {}".format(email))
        else:
            print("[ERROR] Registration Failed :(- {}".format(email))

def main():
    count = input("How many accounts do you want to generate?: ")
    count = int(count)

    for count in range(0,count):
        # Create a random mailbox
        mailbox = create_mailbox()
        # Do initial registration 
        registration(mailbox.address)

if __name__ == "__main__":
    main()

The new version of the script includes a few modifications. To start, the code has been broken down into three functions:

  • create_mailbox handles the communication with OneSecMail and returns a temporary mailbox.
  • registration creates a browser instance and interacts with the signup form.
  • main is the main wrapper for the script. In addition to calling the create_mailbox and registration functions, it specifies how many accounts are to be created.

Try it out by running the following:

python main.py

The console should look something like this:

Console bot

Future development might include additional code for reading the emails and opening the confirmation links; for the sake of brevity, it’s assumed that completing this part would successfully automate the signup process entirely. By integrating Splinter and 1secMAIL in an automated signup bot script, malicious actors can easily bypass the security controls of most website registration forms.

Adding Extra Security with Fingerprint Pro

When traditional security measures fail to prevent unauthorized accounts from being created, Fingerprint Pro can be an incredibly powerful tool for securing websites against bad bots. Its browser fingerprinting technology is the most advanced on the market, combining various cutting-edge methods for uniquely identifying browsers with machine learning algorithms and a probability engine; the result is an astonishing 99.5% accuracy rate.

A browser fingerprint is a set of information related to a user’s device. This includes a device’s hardware, operating system, browser, and configuration. Browser fingerprinting is the process of collecting information through a web browser to build a fingerprint of a device.

Fingerprint uses browser fingerprinting among other techniques to generate a visitorID value. This value is then returned to the application, providing a persistent record of a specific user.

To start implementing Fingerprint Pro, create a new Fingerprint account—it’s free for 14 days and includes unlimited API calls.

After successfully registering, a snippet of code is provided to add to the website:

<script>
  function initFingerprintJS() {
    FingerprintJS.load({apiKey: 'my-public-api-key'})
      .then(fp => fp.get())
      .then(result => console.log(result.visitorId));
  }
</script>
<script
  async
  src="https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs-pro@3/dist/fp.min.js"
  onload="initFingerprintJS()"
></script>

Add the following snippet of code to signup_form/views/signup.hbs

<div class="signup-form container">
  <div class="logo">
    <img src="/images/logo.svg">
  </div>
  <div class="row signup-form-content">
    <div class="col-md-4 offset-md-1">
      <div class="promo-container">
        <h2 class="promo-header">
          Gain free access to <span class="accent">over 9,000
            courses</span> for 14 days!
        </h2>
        <div class="promo-text">
          <p>Learn coding, design, illustration, music production, video editing and tons of other things at
            e-learning.com.
            We have literally hundreds of thousands of hours of content created by some of the best instructors in the world,
            who are experts in their fields.</p>
          <p><strong class="accent">Start learning today!</strong></p>
        </div>
      </div>
    </div>
    <div class="col-md-5 offset-md-1">
      <div class="card">
        <div class="card-body">
          <h2 class="card-title">Sign up for free</h2>
          <p class="auth-switcher-text">Already have an account? <a href="#">Sign in</a></p>

          <form method="POST" action="/signup">
            <div class="form-group">
              <input
                required
                type="email"
                class="form-control {{#if error}}is-invalid{{/if}}"
                id="email"
                name="email"
                placeholder="Email"
                value="{{#if email}}{{email}}{{/if}}"
              >
              {{#if error}}
              <div class="invalid-feedback">
                {{ error }}
              </div>
              {{/if}}
            </div>
            <div class="signup-button-wrapper text-center">
              <button class="btn btn-primary signup-button">Create Account</button>
            </div>
          </form>
          <div class="conditions">
            By signing up, you agree to our
            <a href="#">Terms and Conditions</a>
            and the
            <a href="#">Privacy Policy</a>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<script>
  function initFingerprintJS() {
    FingerprintJS.load({apiKey: 'my-public-api-key'})
      .then(fp => fp.get())
      .then(result => console.log(result.visitorId));
  }
</script>
<script
  async
  src="https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs-pro@3/dist/fp.min.js"
  onload="initFingerprintJS()"
></script>

Verify that the integration is working correctly by reloading the page and checking the browser console. A string like this should appear:

Unique ID

This is the visitorID that can be used to reliably identify the browser, regardless of any masking or blocking attempts. Try opening the signup form in the browser’s incognito mode—notice that the visitorID is the same.

Next, make use of the visitorID value by passing it to the server upon form submission:

<div class="signup-form container">
...
          <form method="POST" action="/signup">
            <div class="form-group">
              <input hidden type="text" id="visitorId" name="visitorId" value=""/>
              <input
                required
                type="email"
                class="form-control {{#if error}}is-invalid{{/if}}"
                id="email"
                name="email"
                placeholder="Email"
                value="{{#if email}}{{email}}{{/if}}"
              >
...
</div>

<script>
  function initFingerprintJS() {
    FingerprintJS.load({apiKey: 'my-public-api-key'})
      .then(fp => fp.get())
      .then(result => {
        document.getElementById('visitorId').value = result.visitorId
      });
  }
</script>
<script
  async
  src="https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs-pro@3/dist/fp.min.js"
  onload="initFingerprintJS()"
></script>

Return to the backend server and verify that the new visitorID value is being used:

// signup form submission, inside index.js at root directory
  app.post('/signup', async function signup(req, res, next) {
    const {email, visitorId} = req.body

    try {
      if (!email) {
        throw new Error('email is required')
      }

      const hasVisitorId = (await client.query('select * from users where visitor_id = $1', [visitorId])).rows.length > 0

      if (hasVisitorId) {
        throw new Error('Looks like you already have an account, please sign in')
      }

      const result = await client.query('insert into users(email, visitor_id) values($1, $2) returning *', [email, visitorId])
      console.log(`${result.rows[0].email} added to the db`)

      res.render('signup_success', {layout: 'index'})
    } catch (e) {
      console.error(e)

      let message = e.message

      if (e.code === '23505') {
        message = 'User with this email already exists'
      }

      res.render('signup', {layout: 'index', fpjsToken: process.env.FPJS_TOKEN, error: message, email})
    }
  })

The code above has been adjusted to:

  • read the visitorID from the request body
  • make a call to the database and check for any existing users with the same visitorID
  • throw an error if a user exists in the system with the same visitorID

If the visitorID is not found in the users table, the script can create a new user with the email and visitorID values provided. Be sure to run the following migration before running the code:

alter table users
    add column visitor_id text null unique;

Trying to create multiple accounts will result in the following error:

Error, already have an account

Run the signup bot against the new form and observe what happens:

asciicast

Even if different email addresses are used, Fingerprint correctly flags the signup attempt as coming from the same visitor. At this point, a website operator could choose to block further sign-up attempts.

Conclusion

While it’s not possible to completely prevent bot attacks given their current sophistication levels, website operators can drastically reduce the risk of compromise by combining techniques like email validation and CAPTCHAS with Fingerprint Pro. This layered approach merges traditional techniques for confirming accounts with a solution that’s 99.5% accurate for identifying unique visitors. And for recognizing bot activity and identifying malicious bots, Botd offers powerful bot detection in an easy-to-use, open source library.

The full code for the examples used in this article can be found here:

Share this post