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:
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
:
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 thecreate_mailbox
andregistration
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:
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:
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:
Run the signup bot against the new form and observe what happens:
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: