Tor: How to block anonymous traffic with Nginx or inside your web application

A report from CloudFlare says that over 94% of all HTTP requests that come from across the Tor browsing network are malicious:

theMiddle
7 min readAug 22, 2017

Online anonymity is important (it enables activists, journalists, and repressed members of the society to speak up freely), but sometimes our private application need to know “who” and “from where” a user is connecting. Moreover, by blocking anonymous traffic, you’ll cut off a huge amount of automatically attacks that your application receives daily.

“Based on data across the CloudFlare network, 94% of requests that we see across the Tor network are per se maliciou. That doesn’t mean they are visiting controversial content, but instead that they are automated requests designed to harm our customers. A large percentage of the comment spam, vulnerability scanning, ad click fraud, content scraping, and login scanning comes via the Tor network.” [CloudFlare Blog]

How to

First of all, in order to block all Tor traffic, you need to know from which IPs Tor users will connect to your website: You need an updated list of Tor exit nodes! Googling around you can find a lot of “Tor exit nodes lists”. Unfortunately, most of them are incomplete or not up to date.

I think you have 2 possibilities: the first one is to get the “official” Tor exit nodes list from torproject.org (https://check.torproject.org/exit-addresses). It’s accurate but, unfortunately, not really up to date. I’ve used that list for a while but, often during some tests, my exit IP wasn’t in that list and I had to enter it manually in order to block my connection from Tor.

Otherwise (IMHO) a better solution is to use a constantly updated API service like the one made by SECTHEMALL (that I use 😉 and recommend 😉 https://secthemall.com/reputation-api/tor). In this post I’ll show you how I block Tor traffic on my web applications using this service in three different ways: Nginx IP deny, ModSecurity, and a PHP code.

Get the list

You can get the last 20 Tor exit nodes IPs from SECTHEMALL without authentication, BUT we need the whole list ’cause we are paranoid dudes! 😷 So, before start you need to register here https://secthemall.com/signup/ and create a free account.

Now, log in to your profile (User menu -> Profile) and click on “Show API Key” which we’ll use to retrieve the whole Tor exit nodes list:

SECTHEMALL Profile page

Now using cURL we can get all results without any limits. The syntax is super easy! with the -u parameter, send your username and API Key as a Basic Authentication:

$ curl -u your-username:your-API-key \
'https://secthemall.com/public-list/tor-exit-nodes/json?size=1000'

The response is something like the following:

SECTHEMALL Tor exit nodes JSON result

You could get a “plain-text” list by replacing “json” in the URL with “iplist”. This can be ok for Nginx or ModSecurity. The only problem with a “plain-text” results, is that you lose the “lastid” value. This parameter tells you if the list has changed since your last download. So, you can save the“lastid” value and check just for it, when it changes you could download the whole list.

Another problem is that for deploy a change on the Tor IP list, Nginx configuration needs to be reloaded. In order to solve this problem, I wrote a Python script that checks the secthemall “lastid” value and downloads the IPs list when needed. After that, the Python script reloads the Nginx configuration automatically.

The Python script

You can install it with the following syntax:

$ mkdir /opt/secthemall-tor/
$ cd /opt/secthemall-tor
$ wget https://goo.gl/TiShB4 -O secthemall-tor.py
$ vi secthemall-tor.py
$ python secthemall-tor.py

Put your SECTHEMALL username and API Key on the configuration part (at the top of the script) and run it in background! It’ll check the “lastid” value each 60 seconds (you can change it). Feel free to use/change this script as you wish.

You’ll find two different files TXT on the secthemall-tor/ directory: The first one “nginx_deny_tor.txt” which you need to include into the Nginx configuration (see later), and the second one “modsecurity_deny_tor.txt” is a plain-text list of IP addresses that you can use as a ModSecurity SecRule (see later).

Nginx configuration

Just include /opt/secthemall-tor/nginx_deny_tor.txt in a location, server or http block, like the following:

location /login {
include /opt/secthemall-tor/nginx_deny_tor.txt;
...
}

If you look inside the nginx_deny_tor.txt file, you’ll see the whole list of Tor exit nodes IPs, each one denied using the Nginx syntax:

deny <ip address / class>;

for example:

$ more nginx_deny_tor.txtdeny 178.32.53.94;
deny 80.82.78.127;
deny 144.217.161.119;
deny 185.10.68.119;
deny 77.250.227.12;
deny 163.172.162.106;
deny 137.74.73.179;
...

ModSecurity SecRule

A little bit different is the implementation with ModSecurity, ’cause you need to create a SecRule that checks the value of REMOTE_ADDR and tries to match the user IP over the modsecurity_deny_tor.txt file, I usually use this:

SecRule REMOTE_ADDR "@ipMatchFromFile /opt/secthemall-tor/modsecurity_deny_tor.txt" "id:6000,\
phase:request,log,\
msg:'Tor exit node',\
tag:'bad-reputation/Tor',\
severity:'CRITICAL',\
maturity:'9',\
accuracy:'9',\
rev:'1',\
ver:'SECTHEMALL_1.0',\
capture,\
drop"

Maybe you don’t want to block Tor on all your website pages, for example if you have a WordPress and you don’t want that “wp-login.php” or “wp-admin” being accessible from anonymous users, you could write a chained SecRule as the following:

SecRule REQUEST_URI "@rx (\/wp\-login|\/wp\-admin).*" "id:6000,\
chain,\
phase:request,\
deny"

SecRule REMOTE_ADDR "@ipMatchFromFile /opt/secthemall-tor/modsecurity_deny_tor.txt" "id:6000,\
phase:request,log,\
msg:'Tor exit node',\
tag:'bad-reputation/Tor',\
severity:'CRITICAL',\
maturity:'9',\
accuracy:'9',\
rev:'1',\
ver:'SECTHEMALL_1.0',\
capture"

Pay attention that, in the chained SecRule, the disruptive “drop” action must be set at the first rule of the chain, otherwise, you’ll spend a lot of time in debugging and swearing at the keyboard. 😡

ModSecurity + CloudFlare

If your application uses CloudFlare, you need to check the CF-Connecting-IP header (no matter if it’s fake and set by user, you just need to block it):

SecRule REQUEST_HEADERS:CF-Connecting-IP "@ipMatchFromFile /opt/secthemall-tor/modsecurity_deny_tor.txt" "id:6001,\
phase:request,log,\
msg:'Anonymous Browsing (Tor)',\
tag:'bad-reputation/Tor-exit-node',\
severity:'CRITICAL',\
maturity:'9',\
accuracy:'9',\
rev:'1',\
ver:'SECTHEMALL_1.0',\
capture,\
drop"

This is true even if your application has a load balancer. For example, using the DigitalOcean Load Balancer service, the real user IP address is the value of X-Forwarded-For header parameter:

SecRule REQUEST_HEADERS:X-Forwarded-For "@ipMatchFromFile /opt/secthemall-tor/modsecurity_deny_tor.txt" "id:6001,\
phase:request,log,\
msg:'Anonymous Browsing (Tor)',\
tag:'bad-reputation/Tor-exit-node',\
severity:'CRITICAL',\
maturity:'9',\
accuracy:'9',\
rev:'1',\
ver:'SECTHEMALL_1.0',\
capture,\
drop"

Keep in mind that, in this case, you don’t really need to check if a request comes from CloudFlare or DigitalOcean. For example, if my goal is to get the “real” user IP and store it in my logs, in this case: yes, you need to check if the “CF-Connecting-IP” has been set by CloudFlare proxy or is set by a malicious user that tries to impersonate a CloudFlare server. You can do it by checking the remote address with the official list of CloudFlare IPv4 and IPv6 classes that you can find here: https://www.cloudflare.com/ips/

But, in our case, what we need is just to block a Tor exit node, no matter if it’s real o fake, in both cases you should block those requests :)

PHP Application

You could use the modsecurity_deny_tor.txt file in order to block Tor directly from you application. An ugly example could be:

<?php$tor = file('/opt/secthemall-tor/modsecurity_deny_tor.txt');if(in_array($_SERVER['REMOTE_ADDR'], $tor)) {
die("Sorry, Tor is not allowed here.");
}
...

Again, if you need to check the IP address on others header parameters, you could use the following syntax:

<?php$tor = file('/opt/secthemall-tor/modsecurity_deny_tor.txt');if(
in_array($_SERVER['REMOTE_ADDR'], $tor) ||
in_array($_SERVER['HTTP_X_FORWARDING_FOR'], $tor) ||
in_array($_SERVER['HTTP_CF_CONNECTING_IP'], $tor)
) {
die("Sorry, Tor is not allowed here.");
}
...

Results

My Web Application Firewall (Nginx + libmodsecurity) stores all logs into an Elasticsearch cluster. The following screenshot shows all blocked HTTP requests from Tor IPs:

It’s odd to see many brute-force attacks, scans, and spam received by my WAF. Even if it doesn’t host big websites like CloudFlare, the smallest and insignificant WordPress is involved in this type of attacks too. The following screenshot shows how many attacks from Tor the WAF received and blocked for CVE-2015–7858 Joomla Vulnerability:

Conclusions

Sometimes, blocking Tor exit nodes makes your web application a little bit secure but is not enough! Please, don’t think that a “reputation database” is enough for cut off bad guys that are trying to exploit a RCE on you vulnerable code. This is just a cool solution to take time and patch your applications! For example, when a 0-day has been found in your favourite blog platform, is good to have time for patching knowing that your ModSecurity and your Reputation DB can mitigate spike of attacks exploiting that vulnerability.

Feel free to ask if you need more information, or tell me if you implemented this solutions on your environment, and how you did!

--

--