angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Schwarze Liste von IP-Adressen auf Ihrer Flask -Website, die auf Linux läuft

16 April 2020 durch Peter
In Flask

Manchmal möchten Sie IP-Adressen sofort blockieren. Dieser Beitrag beschreibt eine Methode, wie Sie dies tun können.

post main image
https://unsplash.com/@vladbahara

Sie haben eine Website und sie funktioniert gut. Aber Sie stellen fest, dass bestimmte Besucher versuchen, mit Ihren Formularen herumzuspielen. Sie kommen von bestimmten IP-Adressen. Dann gibt es auch Bots, die Ihre Website scannen. Einige sind notwendig, aber andere sollten wegbleiben. Hassen Sie das nicht auch? Doch, ich hasse es. In der Vergangenheit habe ich einmal ein Modul geschrieben, das eine nicht so nette Antwort sehr langsam, Byte für Byte, zurückgab und ihre Systeme verlangsamte. Oder eine nicht enden wollende Menge an Daten zurückgab. Aber das ist eine andere Geschichte.

Vorerst möchte ich mich auf eine andere Methode konzentrieren: die Blockierung dieser Anträge. Geben Sie einfach ein HTTP 403 Forbidden zurück. Ich möchte in der Lage sein, dies on-the-fly von meinem Website-Administrationsbereich aus zu tun. Dort geben wir die IP-Adressen oder den Bereich von IP-Adressen an, die wir blockieren wollen. Es gibt auch andere Möglichkeiten dazu, wie die Verwendung von .htaccess -Dateien und Web-Servereinstellungen. Ich werde sie am Ende dieses Beitrags erwähnen.

Mehrere Gründe zu blockieren

Ich habe bereits erwähnt, dass einer der Gründe, den Zugang zu Ihrer Website zu blockieren, darin besteht, böswillige Besucher zu blockieren. Sie wollen sehen, wie sie Ihre Site kaputtmachen, Ihren Kommentarbereich mit Werbung oder anderen verrückten Botschaften vollstopfen können. Es gibt viele Gründe, warum sie das tun, ich glaube, einer davon ist, dass sie Sie zwingen wollen, sich ein Anti-Spam-Plugin eines Dritten zu schnappen. Diese können sehr effektiv sein, da sie sich mit riesigen Datenbanken mit Spam-Informationen verbinden. Aber wenn wir die Privatsphäre unserer Besucher respektieren wollen, können wir ein solches Plugin nicht verwenden. Wir müssen andere Wege gehen, und ein letzter Ausweg ist oft die Sperrung von IP-Adressen.

Es kann auch notwendig sein, bestimmte Bots zu blockieren, die Ihre Site scannen. Einige Bots erzeugen verrückte Mengen an Datenverkehr. Ich habe den gesamten Verkehr zu dieser Website für einen bestimmten Zeitraum überprüft, und es stellte sich heraus, dass nur 10%, wahrscheinlich sogar noch weniger, der Anfragen von echten Besuchern kamen! Natürlich sind nicht alle Bots schlecht, aber einige halten sich wirklich nicht an die Regeln. Die meisten Bots können anhand der User-Agent-Zeichenfolge identifiziert werden. Ich habe die folgenden beiden gefunden, die ich wirklich blockieren möchte:

  • SemrushBot
  • AhrefsBot

Seien Sie vorsichtig, was zu blockieren ist, viele Bots werden verwendet, um Ihre Website in den Suchmaschinenergebnissen zu erhalten. Bei SemrushBot geht es um SEO, das benutze ich im Moment nicht. Das Blockieren von User Agents wird in diesem Beitrag nicht behandelt. Er wird sich nicht so oft ändern, und Sie können Blöcke auf andere Weise setzen.

Gutes über unerwünschte Anfragen

Wenn Sie eine ordnungsgemäße Protokollierung implementieren, können Sie auch unerwünschte Anfragen ausnutzen. Die folgende Liste zeigt einige Anfragen, die einen HTTP 404-Fehler für diese Site verursacht haben:

http://peterspython.com/css/album.css
http://www.peterspython.com/wordpress
http://peterspython.com/blog/wp-includes/wlwmanifest.xml
http://peterspython.com/wordpress/wp-includes/wlwmanifest.xml
http://peterspython.com/website/wp-includes/wlwmanifest.xml
http://peterspython.com/public/ui/v1/js/sea.js
http://www.peterspython.com/public/ui/v1/js/sea.js
http://peterspython.com/vendor/phpunit/phpunit/phpunit.xsd
http://peterspython.com/vendor/phpunit/phpunit/LICENSE
http://www.peterspython.com/apple-touch-icon.png
http://peterspython.com/humans.txt
http://peterspython.com/license.txt

Wir sehen, dass Bots nach der Datei wlwmanifest.xml suchen. Dies scheint eine Datei zu sein, die mit "Windows Live Writer" assoziiert ist, einer von Microsoft entwickelten Anwendung zur Veröffentlichung von Blogs, die 2017 eingestellt wurde und möglicherweise anfällig ist. Ein weiterer Angriff sucht nach PHPUnit, einer PHP -Einheit, die framework testet. Diese enthielt eine Schwachstelle, die möglicherweise noch nicht gepatcht wurde. Andere Angriffs-Bots können URLs generieren, die einen HTTP 500-Fehler verursachen. Dies kann beabsichtigt sein, kann aber auch durch Schwächen Ihrer Website verursacht werden.

Die gute Nachricht ist, dass Sie diese Informationen nutzen können, um Ihre Website zu verbessern. Achten Sie immer auf eine korrekte Protokollierung, Fehler geben Ihnen sehr wertvolle Informationen!

Beschränkung auf die IP-Adressen IPv4

Das Blockieren von Besuchern anhand der IP-Adresse hat seine Grenzen. Viele Menschen im Internet erhalten ihre IP-Adresse, wenn sie sich über DHCP mit einem Server verbinden. Dies gilt vor allem für Mobiltelefone. Seien Sie also vorsichtig, was zu blockieren ist.

Dann gibt es noch IPv6 , das entwickelt wurde, um die begrenzte Verfügbarkeit von IPv4 -Adressen zu überwinden. Obwohl in einigen Berichten angegeben wird, dass 30 % des Internetverkehrs auf IPv6 entfallen, ist die Anzahl der Server, die IPv6 tatsächlich aktiviert haben, weitaus geringer. Dies bedeutet glücklicherweise, dass es im Moment keinen Grund gibt, Ihren Server auf IPv6 zu migrieren. Das Blockieren von Spam mit IPv6 ist mit dieser Methode möglich, aber es gibt einen Haken.

Verwaltungsvorgänge und Regeln für IP-Adressen auf der Schwarzen Liste

Im Admin möchte ich die IP-Adressen angeben, die ich auf die schwarze Liste setzen möchte. Es gibt eine Tabelle mit Datensätzen von IP-Adressen auf der schwarzen Liste. Für IP-Adressen möchte ich die IP-Adressen wie folgt angeben können:

  1. Eine einzelne IP-Adresse, Beispiel: 1.2.3.4
  2. Ein IP-Netzwerk, Beispiel: 1.2.3.0/24
  3. Einen IP-Adressbereich, Beispiel: 1.2.3.6-1.2.4.2

Ich spezifiziere eine davon in einem einzigen Datensatz und nenne dies eine "Regel für IP-Adressen auf der Schwarzen Liste".

Caching zur Vermeidung von Datenbankzugriffen

Wir wollen sicherlich nicht bei jeder Anfrage auf die Datenbank zugreifen, um zu sehen, ob die Anfrage erlaubt ist. Das würde die Anfragen verlangsamen. Aus diesem Grund verwenden wir Caching. Anstatt die Datenbank abzufragen, überprüfen wir zuerst den Cache, um zu sehen, ob die IP-Adresse zuvor auf die Website zugegriffen hat. Für jede IP-Adresse haben wir ein Flag namens 'erlaubt'. Wenn True, dann ist der Zugriff erlaubt, wenn False, dann wird der Zugriff blockiert.

Wenn sich die IP-Adresse im Cache befindet, sind wir fertig, fahren wir fort oder blockieren wir. Wenn sich die IP-Adresse nicht im Cache befindet, prüfen wir, ob sie in den Regeln für IP-Adressen auf der Schwarzen Liste steht. Das Ergebnis wird dem Cache hinzugefügt, und wenn das nächste Mal eine Anfrage mit dieser IP-Adresse auf unsere Site trifft, befinden sich die Daten im Cache und die Datenbank soll nicht abgefragt werden.

Hinzufügen und Entfernen von Regeln für IP-Adressen auf der Schwarzen Liste

Angenommen, wir haben Hunderte, Tausende von Objekten in unserem Cache. Jetzt wollen wir mit Hilfe des Administrators Änderungen vornehmen, indem wir entweder eine Regel für IP-Adressen auf der Schwarzen Liste hinzufügen oder eine Regel für IP-Adressen auf der Schwarzen Liste entfernen.

Das Hinzufügen oder Entfernen einer Regel ist nicht trivial, da die Regel IP-Adressen enthalten kann, die sich bereits im Cache befinden. Was soll mit unseren zwischengespeicherten Werten geschehen? Am einfachsten ist es, den Cache zu leeren und ihn wieder neu aufbauen zu lassen. Dadurch werden die nächsten Anfragen für kurze Zeit verlangsamt. Die einzige andere Möglichkeit besteht darin, die IP-Adressen im Cache zu scannen und zu prüfen, ob sie mit der hinzugefügten oder entfernten Regel für IP-Adressen auf der Schwarzen Liste übereinstimmen. Wenn sie übereinstimmen, entfernen wir sie aus dem Cache. Ich habe einige Ideen, wie dies zu implementieren ist, habe dies aber noch nicht getan.

Hinzufügen von Zeitstempeln

Für maximale Leistung sind die IP-Adressen-Informationen im Cache schreibgeschützt und verfallen nicht. Das bedeutet, dass sie mit der Zeit riesig werden können, wenn Sie viele Besucher haben. Da die meisten Besucher auf Ihre Website nur wenige Minuten lang zugreifen, können wir den im Cache gespeicherten IP-Adressen einen Zeitstempel hinzufügen, der bei jedem Zugriff aktualisiert wird. Der Zeitstempel macht es einfach, alte Einträge zu entfernen.

Anfragen zum gleichen Zeitpunkt

Angenommen, zwei Anfragen, Anfrage A und Anfrage B, kommen zur gleichen Zeit an, wobei beide die gleiche IP-Adresse verwenden. Wenn sie sich nicht im Cache befinden, prüfen beide, ob ihre IP-Adresse blockiert ist, indem sie die Tabelle der IP-Adressregeln der Blacklist durchsuchen. Dann aktualisieren beide das cached_access-Element. Antrag A erstellt zunächst das zulässige Element. Aber dann erzeugt Antrag B das erlaubte Element und überschreibt das erlaubte Element von Antrag A. Dasselbe gilt, wenn wir den Zeitstempel des zwischengespeicherten Elements aktualisieren wollen. Das mag schlecht aussehen, ist aber nicht wirklich so schlecht. Wir müssen nur sicherstellen, dass die Erstellungsoperation atomar ist.

Verwendung des Dateisystems Linux als Cache

Für den Moment habe ich mich dafür entschieden, den Cache mit Dateien zu implementieren. Das Dateisystem Linux ist schnell genug, um dies für meine Anwendung zu handhaben. Ich möchte nicht etwas wie Redis hinzufügen, ich möchte die Abhängigkeiten minimal halten.

Wenn wir eine 'erlaubte' Datei pro IP-Adresse haben, dann kann die Datei klein sein, der Inhalt ist 0 (blockiert) oder 1 (erlaubt). Um eine große Anzahl von Dateien in einem Verzeichnis und langsames Nachschlagen zu verhindern, erstellen wir Unterverzeichnisse auf der Basis der IP-Adresse. Wir teilen die IP-Adresse durch den Punkt ('.') auf und verwenden diese zur Erstellung von Verzeichnissen. Der Zeitstempel der 'erlaubten' Datei ändert sich automatisch, wenn die Datei gelesen wird. In Linux haben wir die folgenden Zeitstempel:

  • mtime (ls -l)
    Das letzte Mal, als der Dateiinhalt geändert wurde
  • ctime
    Das letzte Mal, als sich der Dateistatus, z.B. Berechtigungen, geändert hat
  • atime (ls -lu)
    Das letzte Mal, als die Datei gelesen wurde

Für unseren Zweck können wir atime als Zeitstempel verwenden. Wir müssen die Uhrzeit der Datei nicht aktualisieren. Es gibt ein Problem, wenn Sie in der Lage sein wollen, den Inhalt der erlaubten Dateien im Admin anzuzeigen. Dies würde die Dateien lesen und die Zeitstempel ändern. Wir können dies umgehen, indem wir eine Kopie der "erlaubten" Datei erstellen. Das Lesen der Kopie ändert nicht die Zeit der ursprünglichen 'erlaubten' Datei.

Eine Warnung bei Verwendung der Zugriffszeit Linux atime

Es gibt im Internet viele Informationen über Linux -Zeitstempel, aber nur sehr wenige erwähnen, dass dies möglicherweise nicht wie erwartet funktioniert. Ich lade Sie ein, sich dazu die untenstehenden Links anzusehen. Mit diesem Befehl können Sie z.B. überprüfen, ob relatime eine Mount-Option ist:

cat /proc/mounts | grep relatime

Die Zusammenfassung lautet:

  • Die Aktualisierung der Zeit bei jedem Lesen ist aus Performance-Gründen standardmäßig deaktiviert.
  • Seit Kernel 2.6.30 ist relatime die Standardoption
  • Seit Kernel 2.6.30 wird die letzte Zugriffszeit einer Datei immer aktualisiert, wenn sie mehr als 1 Tag alt ist.

Das bedeutet, dass wir immer noch die Zeit nutzen können, aber eine Resolution von einem Tag respektieren müssen. Kein Problem für mich, aber warten Sie, lassen Sie uns testen, ob das wirklich funktioniert.

ls -l

Das Ergebnis ist:

total 8
-rw-r--r-- 1 flaskuser  flaskgroup 1 apr 16 15:47 allowed
-rw-r--r-- 1 flaskuser  flaskgroup 1 apr 16 15:47 allowed_copy

Als nächstes wollen wir die aZeit oder Zugriffszeit der erlaubten Datei sehen:

stat allowed

Das Ergebnis lautet:

  File: allowed
  Size: 1         	Blocks: 8          IO Block: 4096   regular file
Device: 806h/2054d	Inode: 38805116    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1002/flaskuser)   Gid: ( 1002/flaskgroup)
Access: 2020-04-16 15:47:06.024559817  +0200
Modify: 2020-04-16 15:47:06.024559817  +0200
Change: 2020-04-16 15:47:06.024559817  +0200
 Birth: -

Jetzt ändern wir die Zugriffszeit auf den Vortag:

sudo touch -a -t 202004151530.02 allowed

Das Ergebnis des Befehls stat zeigt, dass die Zugriffszeit einen Tag früher liegt:

  File: allowed
  Size: 1         	Blocks: 8          IO Block: 4096   regular file
Device: 806h/2054d	Inode: 38805116    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1002/flaskuser)   Gid: ( 1002/flaskgroup)
Access: 2020-04-15 15:30:02.000000000  +0200
Modify: 2020-04-16 15:47:06.024559817  +0200
Change: 2020-04-16 15:52:12.472562630  +0200
 Birth: -

Nun generieren wir eine Anfrage auf der Website und führen nach der Anfrage erneut den Befehl stat aus:

  File: allowed
  Size: 1         	Blocks: 8          IO Block: 4096   regular file
Device: 806h/2054d	Inode: 38805116    Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1002/flaskuser)   Gid: ( 1002/flaskgroup)
Access: 2020-04-16 15:56:24.200564941  +0200
Modify: 2020-04-16 15:47:06.024559817  +0200
Change: 2020-04-16 15:52:12.472562630  +0200
 Birth: -

Die Zugriffszeit wurde auf heute aktualisiert. Nachfolgende Anfragen aktualisieren die Zugriffszeit nicht mehr. Bei der Arbeit wie erwartet habe ich heute etwas gelernt.

Details zur Implementierung

Ich nannte die Klasse CachedAccess. In Flask's before_request instanziiere ich sie wie folgt:

    @app.before_request
    def  before_request():
        ...
        g.ip_address = get_ip_address()
        ...
        cached_access = CachedAccess()
        if not cached_access.is_allowed():
            # bye bye
            abort(403)

Und hier sind die (wichtigen) Teile der Klasse:

class CachedAccess:

    def __init__(self):
        ...


    def log_block(self, reason):
        ...


    def is_allowed_ip_address(self, ip_address_uint):

        # check: single ip addresses
        access_block_ip_address = db_select(
            model_class_list=[AccessBlockIPAddress],
            filter_by_list=[
                (AccessBlockIPAddress, 'is_active', 'eq', True),
                (AccessBlockIPAddress, 'ip_address_type', 'eq', 3),
                (AccessBlockIPAddress, 'ip_address_uint', 'eq', ip_address_uint),
            ],
        ).first()

        if access_block_ip_address is not  None:
            # found
            return False

        # check: network and range
        access_block_ip_address = db_select(
            model_class_list=[AccessBlockIPAddress],
            filter_by_list=[
                (AccessBlockIPAddress, 'is_active', 'eq', True),
                (AccessBlockIPAddress, 'ip_address_type', 'in', [1, 2]),
                (AccessBlockIPAddress, 'ip_address_from_uint', 'le', ip_address_uint),
                (AccessBlockIPAddress, 'ip_address_to_uint', 'ge', ip_address_uint),
            ],
        ).first()

        if access_block_ip_address is not  None:
            # found
            return False

        return True


    def is_allowed(self):

        # check if valid ip_address
        try:
            ip_address_uint = int( ipaddress.ip_address(g.ip_address) )
        except Exception as e:
             current_app.logger.error(fname  +  ': not a valid ip address = {}, {}'.format(g.ip_address, str(e)))
            return True

        # create ip_address_file
        app_cached_access_dir =  current_app.config['APP_CACHED_ACCESS_DIR']
        ip_address_parts = g.ip_address.split('.')
        ip_address_file = os.path.join(app_cached_access_dir, *ip_address_parts, 'allowed')

        # check if file exists and read its contents
        found = True
        try:
            with open(ip_address_file, 'r') as f:
                allowed = f.read()
        except:
               found = False

        if found:
            # done
            if allowed == '1':
                return True
            self.log_block(1)
            return False

        # check if g.ip_address matches a rule in blacklisted IP addresses table
        allowed = self.is_allowed_ip_address(ip_address_uint)

        # create directories for g.ip_address
        ip_address_dir = os.path.dirname(ip_address_file)
        try:
            pathlib.Path(ip_address_dir).mkdir(parents=True, exist_ok=True)
        except Exception as e:
             current_app.logger.error(fname  +  ': error creating directories ip_address_dir = {}, {}'.format(ip_address_dir, str(e)))
            return True

        # create allowed temp file
        temp_name = next(tempfile._get_candidate_names())
        ip_address_temp_file = os.path.join(app_cached_access_dir, *ip_address_parts, temp_name)
        try:
            with open(ip_address_temp_file, 'w') as f:
                f.write( '1' if allowed else '0' )
        except Exception as e:
             current_app.logger.error(fname  +  ': error writing ip_address_temp_file = {}, {}'.format(ip_address_temp_file, str(e)))
            return True

        # atomic move ip_address_temp_file to ip_address_file
        try:
            os.rename(ip_address_temp_file, ip_address_file)
        except Exception as e:
             current_app.logger.error(fname  +  ': error renaming ip_address_temp_file = {} to  ip_address_temp_file = {}, {}'.format(ip_address_temp_file, ip_address_temp_file, str(e)))
            return True

        if allowed:
            return True

        self.log_block(2)
        return False

Das ist nicht wirklich sehr schwierig. Ich wandle die IP-Adresse in ein Unsigned Int um, damit wir prüfen können, ob sie in einem IP-Netzwerk oder IP-Adressbereich liegt. Wenn ein unerwarteter Fehler auftritt, protokolliere ich den Fehler und lasse die IP-Adresse zu. Das bedeutet, dass wir unerwartete Anfragen nicht blockieren.

Entwicklung und Produktion

Bei der Entwicklung werden Sie wahrscheinlich viele Anfragen während der Tests blockiert sehen. Der Grund dafür ist, dass Bilder, Javascript -Dateien usw. auch vom Flask -Entwicklungsserver bedient werden. Sie können diese Anfragen in Ihrem Code filtern:

    if request_path.startswith( ('/static/') ):
        return

Bei der Produktion gehe ich davon aus, dass Sie Ihren gesamten statischen Inhalt über den Webserver, Nginx, Apache, bereitstellen, was bedeutet, dass keine Zeit verschwendet wird. Wir blockieren nur Anfragen an den Code, Bilder usw. werden nicht blockiert.

Blockierung mit Nginx

Ich wollte meinen Webserver Nginx nicht kontrollieren, um es einfach zu halten. Aber es ist nicht so schwierig, ihm zu sagen, dass er Anfragen blockieren soll. Wenn Sie Nginx verwenden, können Sie ein paar Zeilen hinzufügen, um mehrere user -Agenten wie folgt zu blockieren:

    if ($http_user_agent ~* (wget|curl|libwww-perl) ) {
        return 403;
    }

Und um mehrere IP-Adressen zu blockieren, die Sie verwenden können:

    location / {
        deny 127.0.0.1; # Individual IP Address
        deny 1.2.3.0/24; # IP network
    }

Aber das ist nicht das, was wir wollen. Wir wollen eine dynamische Blockierung. Dazu gibt es mehrere Möglichkeiten, aber natürlich müssen Sie sich viel mehr mit den Besonderheiten von Nginx befassen. Im Internet gibt es genug Beispiele dafür, wie man dies tun kann.

Zusammenfassung

Ich wollte unbedingt eine fliegende Sperrung von IP-Adressen implementieren, und es schien nicht so schwierig zu sein. Im Moment habe ich noch nicht alles implementiert. Das bedeutet keine intelligenten Aktualisierungen nach dem Hinzufügen oder Entfernen von Regeln für IP-Adressen auf der Schwarzen Liste. Stattdessen habe ich eine Schaltfläche 'Cache leeren', die ich drücken kann, nachdem ich Änderungen an der Blacklist-Tabelle vorgenommen habe. Das ist wie ein 'rm -R' in Python.

Der Zeitstempel für die Zugriffszeit von Linux verzögerte mich beim Schreiben dieses Beitrags, ich habe die Zugriffszeit nie verwendet, aber jetzt kenne ich ihre Eigenheiten. Linux aktualisiert die Zugriffszeit einmal am Tag, das ist für mich in Ordnung.

Ich bezweifle, dass Sie eine bessere Leistung erzielen können, aber Sie sollten sich vielleicht andere Optionen wie die Zwischenspeicherung des Elements im Speicher ansehen. Sie könnten TTLCache aus den Python -Cachetools verwenden.

Links / Impressum

cachetools
https://pypi.org/project/cachetools/

Dynamic Blacklisting of IP Addresses
https://docs.nginx.com/nginx/admin-guide/security-controls/blacklisting-ip-addresses/

flask-ipban
https://github.com/Martlark/flask-ipban

flask-ipblock
https://github.com/closeio/flask-ipblock

flask-limiter
https://github.com/alisaifee/flask-limiter

how to know if noatime or relatime is default mount option in kernel?
https://superuser.com/questions/318293/how-to-know-if-noatime-or-relatime-is-default-mount-option-in-kernel

Why is cat not changing the access time?
https://superuser.com/questions/464290/why-is-cat-not-changing-the-access-time/464737#464737

Mehr erfahren:
Flask

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.