In the battle against spammers, Cloudflare's Turnstile emerges as a powerful defense mechanism

Published on
7 mins read
––– views
thumbnail-image

Problem

This week, I received an inquiry through one of the websites I manage. The initial inquiry appeared innocent and general. However, what followed was unexpected. It all began with a harmless question about the cost of one of the services offered at Tinkacode, but it was written in German. In response, we explained that since we were not based in Germany, the service could only be offered remotely. Little did I know that this would open a Pandora's box.

It seems the spammer was testing the waters, seeking confirmation that they were interacting with real humans. Our system was configured to send email notifications to the admin with a summary of each inquiry whenever a user submitted one. This was intended to facilitate quick responses via email and maintain a backup of inquiries. However, the spammer, upon realizing they were communicating with a human on the other end, unleashed a barrage of inquiries, leading to an email flood and unnecessary database entries.

It's worth noting that this isn't the first time such an incident has occurred with one of the websites I manage. A similar occurrence took place with another website in 2022. In that case, my solution was to completely disable the inquiry route. This decision was influenced by the fact that we weren't receiving a significant number of inquiries through the website, and users also had the option to contact us directly via email or phone.

Solution

Last time this happened, I felt defeated and resorted to simply disabling the inquiry route. However, this time, I was determined to find a more sustainable solution that would not only address the current issue but also prevent such incidents in the future.

My initial response was to check whether I had properly implemented CSRF tokens to deter bot traffic. To my relief, the CSRF token implementation was solid and covered all POST requests. However, the situation was becoming increasingly frustrating as the spammer continued to inundate us with approximately 10 inquiries per minute. This surge in activity was likely due to a basic anti-bot service I had put in place through Cloudflare DNS management.

It was at this point that I remembered Cloudflare, a service I frequently rely on for my website and web app setups, offers a CAPTCHA solution called 'turnstile.' What appealed to me about 'turnstile' was its ability to primarily rely on non-interactive challenges, resorting to interactive challenges only when necessary. With this in mind, I made the decision to implement 'turnstile' for all the forms submitted through the platform.

Implementation process

The process of implementing Cloudflare Turnstile is straightforward and well-documented on their website. I'll summarize the process into three main steps:

Getting Site key and secret

The site keys and secrets play a crucial role in the Turnstile integration, serving to identify the domain and validate challenge results and other critical operations. The site key is utilized for the front-end implementation, while the secret remains confidential and is used exclusively on the backend.

These credentials can be obtained from the Cloudflare dashboard by navigating to 'Turnstile,' selecting 'Add a site' for new sites, or 'Select the target domain' for multiple domains. Then, access the 'Settings' section, where you'll find the necessary keys.

Front end intergration

There are two methods for integrating Turnstile on the front-end side: explicit rendering and implicit rendering.

  1. Explicit Rendering: In this approach, you are responsible for rendering the challenge. This can be useful when you want to selectively render the challenge for specific users, such as rendering it only for users who are not logged in. You have full control over when and how the challenge is displayed.

  2. Implicit Rendering: With implicit rendering, you leave the rendering process to Cloudflare as long as there is a div element with the class cf-turnstile. Cloudflare takes care of rendering the challenge when necessary. This approach simplifies the integration process, as you don't need to handle the rendering yourself.

In both cases, you need to include the JavaScript source in the header of your web page for Turnstile integration.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

In my case, I followed Cloudflare's example to protect forms and added a div with the appropriate class name (cf-turnstile) to all my critical forms.


<form action="/login" method="POST">
   <input type="text" placeholder="username"/>
   <input type="password" placeholder="password"/>
   <div class="cf-turnstile" data-sitekey="<YOUR_SITE_KEY>"></div> <!--The added div-->
   <button type="submit" value="Submit">Log in</button>
</form>

Backend intergration

In this case, the backend was implemented using the Flask framework. As a result, token verification was performed using the Python programming language. To streamline the process, I created a wrapper that could be easily added to the top of the target routes.


def check_turnstile(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        if request.method != "POST": #If the request is not a post then there is no need for checking for a turnstile. Return
            return func(*args, **kwargs)
        response_obtained = request.form.get("cf-turnstile-response")
        print(response_obtained) #Just to check if we are receiving a response. Should be removed for production
        if not response_obtained:
            return "Invalid Turnstile"

        # Define the URL and data to send
        url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' # Cloudflare url to verify the validity of the turnstile. 
        data = {
            'secret': 'YOUR_WESITE_SECRET_BEST_TO_LOAD_FROM_CONFIG',
            'response': response_obtained
        }

        # Send a POST request with the data
        response = requests.post(url, data=data)

        # Check the response status
        if response.status_code == 200:
            data = response.json()
            print(data)# Also for debugging purposes
            status = data.get('success')
            if not status or status != True:
                return "Invalid Turnstile"
        else:
            print(f"HTTP error! Status: {response.status_code}") # Check the status code received 
            return "Turnstile verification error"

        return func(*args, **kwargs) 

    return decorated_view

With this decorator, it was possible to add it to any post route as follows


@app.route("sample-route", methods=["POST"])
@check_turnstile
def sample_route():

    data=request.form.get("name")
    # The rest of the function continues

This was the full implementation of the turnstile. I intend to intergrate it into other elements of the site.

Possible challenge

The Turnstile can only be accessed through the designated domain, which can make local testing complicated. However, Cloudflare provides a secure tunnel that can be used to access local services. I discussed the tunnel in this post.

Conclusion

This method proved highly successful in thwarting the bot used by the spammer to send inquiries. The influx of inquiries ceased almost immediately after implementing the Turnstile. This marked a significant achievement, and I plan to incorporate it into most of the web apps I develop in the future.

However, I also acknowledge that some users find CAPTCHAs annoying. That's why I opted for the Cloudflare option, as it primarily employs non-interactive challenges and is implemented only when necessary, striking a balance between security and user experience.