Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
django-defender — ブルートフォースによるログイン試行をブロックする、シンプルで超高速なDjango再利用可能アプリ | Kitploit
ツール/GitHubGitHub/jazzband/django-defender
防御ツールパスワード攻撃ウェブセキュリティ認証
GitHubjazzband/django-defender

django-defender

ブルートフォースによるログイン試行をブロックする、シンプルで超高速なDjango再利用可能アプリ

リポジトリを見る
1.1k1446ヶ月前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

django-defender

.. image:: https://jazzband.co/static/img/badge.svg :target: https://jazzband.co/ :alt: Jazzband

.. image:: https://img.shields.io/pypi/pyversions/django-defender.svg :alt: 対応Pythonバージョン :target: https://pypi.org/project/django-defender/

.. image:: https://img.shields.io/pypi/djversions/django-defender.svg :target: https://pypi.org/project/django-defender/ :alt: 対応Djangoバージョン

.. image:: https://github.com/jazzband/django-defender/workflows/Test/badge.svg :target: https://github.com/jazzband/django-defender/actions :alt: GitHub Actions

.. image:: https://codecov.io/gh/jazzband/django-defender/branch/master/graph/badge.svg :target: https://codecov.io/gh/jazzband/django-defender :alt: カバレッジ

.. image:: https://readthedocs.org/projects/django-defender/badge/?version=latest :alt: ドキュメントステータス :target: https://django-defender.readthedocs.io/en/latest/?badge=latest

ログイン試行のブルートフォース攻撃をブロックする、シンプルなDjango再利用可能アプリです。
目標は可能な限り高速に動作させることで、ログイン処理を遅くしないことです。

データベースへの問い合わせを毎回行わずに済むよう、キャッシュを使用します。最初のバージョンはRedisベースですが、将来的には設定可能にして、ユーザーが自身の環境に最適なバックエンドを選択できるようにすることを目指しています。

django-defender を使用しているサイト

あなたのサイトでdefenderを使用している場合は、PRを送信してリストに追加してください。

  • https://hub.docker.com
  • https://www.mycosbuilder.com

ドキュメント

ドキュメントはRead the Docsで公開されています:

https://django-defender.readthedocs.io

機能

  • すべてのログイン試行をデータベースに記録

  • IPアドレスに異なるヘッダーを使用するリバースプロキシに対応

  • 以下の条件に基づくレート制限

    • ユーザー名
    • IPアドレス
  • ブラックリストにRedisを使用

  • 設定項目

    • Redisサーバー

      • ホスト
      • ポート
      • データベース
      • パスワード
      • キープレフィックス
    • ブロック時間

    • ブロック前の不正試行回数

  • コードカバレッジ95%

  • 完全なドキュメント

  • ログイン試行をデータベースに保存する機能

  • ログイン試行データベーステーブルをクリーンアップする管理コマンド

  • 管理ページ

    • ブロックされたユーザー名とIPアドレスの一覧
    • 最近のログイン試行の一覧
    • ユーザーのブロック解除機能
  • カスタム認証方法に容易に適応可能

  • ユーザー名またはIPをブロックする際にシグナルが送信される

管理ページ


.. image:: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png :target: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png :alt: 管理画面のスクリーンショット

.. image:: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png :target: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png :alt: 管理画面のスクリーンショット

要件

  • Python: 3.8, 3.9, 3.10, 3.11, 3.12, PyPy
  • Django: 3.2, 4.2, 5.0, 5.1, 5.2
  • Redis: 5.x, 6.x, 7.x

インストール

コードをダウンロードし、方法に応じて以下のいずれかの方法でセットアップを実行します。

PyPIからプロダクション対応バージョンをインストールする場合:

.. code-block:: bash

pip install django-defender

ソースコードから開発バージョンをインストールする場合(ダウンロード後):

.. code-block:: bash

python setup.py install

GitHubリポジトリからmasterブランチの開発バージョンをインストールする場合:

.. code-block:: bash

pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev

まず、このプロジェクトを settings.py の INSTALLED_APPS リストに追加する必要があります。

.. code-block:: python

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', # ... 'defender', # ... ]

次に、FailedLoginMiddleware ミドルウェアをインストールします。

.. code-block:: python

MIDDLEWARE_CLASSES = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'defender.middleware.FailedLoginMiddleware', ]

Django管理画面でブロックされたユーザーを管理したい場合は、以下の内容を urls.py に追加してください。

.. code-block:: python

urlpatterns = [ path('admin/defender/', include('defender.urls')), # defender admin path('admin/', admin.site.urls), # normal admin # your own patterns follow... ]

マイグレーション


運用に必要なテーブルをデータベースに作成する必要があります。

.. code-block:: bash

python manage.py migrate defender

管理コマンド


cleanup_django_defender

トラフィックの多いウェブサイトでは、AccessAttempts テーブルがすぐに一杯になります。
データを監査目的で保持する必要がない場合は、テーブルをクリーンに保つための管理コマンドがあります。

このコマンドは、DEFENDER_ACCESS_ATTEMPT_EXPIRATION 設定を見て、どのレコードを削除するかを判断します。
指定がない場合のデフォルトは24時間です。

.. code-block:: bash

$ python manage.py cleanup_django_defender

これを日次または週次のcronジョブとして設定し、テーブルサイズを抑えることができます。

.. code-block:: bash

毎朝12:24に実行

24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log

長期的な目標

  • プラグイン可能なバックエンド(Redis以外も使用可能に)
  • アカウントがブロックされた際にユーザーにメール送信
  • ブロックしないユーザー名やIP(管理者など)のホワイトリスト追加
  • IPアドレスの恒久的ブラックリスト追加
  • 既知のプロキシIPをスキャンし、それらのIPからのリクエストはブロックしないようにする(正当なIPがブロックされる可能性を低減)
  • 古い(設定可能な期間の)ログイン試行を削除する管理コマンド追加

パフォーマンス


defender の目標は、ログインプロセスを遅くしないよう可能な限り高速にすることです。
目標を達成しているか確認するため、アプリケーションをテストする方法が必要です。
最良の方法は、通常のDjangoログインとdefender、django-axesの速度を比較することです。

通常のDjangoログインがベースラインであり、追加のチェックがないため、3つの方法の中で最も高速であると予想されます。
defenderを使用したログインはおそらくDjangoログインより遅くなりますが、django-axesよりは高速であることを期待します。
目標は、通常の生のログインとdefenderとの差をできるだけ小さくすることです。

django-axesのログイン速度は、チェックが多くデータベースクエリも多いため、3つの中で最も遅くなるでしょう。

ログイン速度を測定する最良の方法は、各セットアップでアプリケーションに対して負荷テストを行い、各タイプのログイン時間を比較することです。

負荷テスト


すべてのタイプのログインをカバーするため、負荷テストでは複数のテストを行う必要があります。

#. すべて成功:成功ログインのみの負荷テスト。

#. 混合(一部成功、一部失敗):成功と失敗を混在させ、失敗がパフォーマンスに与える影響を確認。

#. すべて失敗:すべて失敗ログインの負荷テストで、パフォーマンスの違いを確認。

負荷テスト用のサンプルアプリケーションが必要で、唯一の違いはdefender、axes、またはどちらも使用しない設定のみとします。

ホスト型の負荷テストサービスやjmeterのようなツールを使用できます。
いずれの場合でも、すべてのテストで一貫性を保つ必要があります。
jmeterを使用する場合は、他の人が自分でテストを実行できるように、jmeterの設定ファイルも提供すべきです。

負荷テストの結果


結果はここに掲載します。各テストの説明と、結果をグラフとともに示します。

なぜdjango-axesではないのか

django-axesは優れていますが、すべてをデータベースに格納するため、データ量が多いとボトルネックになります。
認証リクエストが200〜300msも遅くなることがあります。
サイトによってはそれほど問題にならないかもしれませんが、他のサイトでは長すぎます。

これはdjango-axesのフォークとして始まり、可能な限りコードを再利用し、不要な部分を削除し、ルックアップを高速化してログインを改善しています。

django-defenderの仕組み

#. 誰かがログインしようとすると、まずそのユーザーが現在ブロックされているかどうかを確認します。
使用しようとしているユーザー名とIPアドレスをチェックします。ブロックされていれば手順5へ。ブロックされていなければ手順2へ。

#. ブロックされていないので、ログインが有効かどうかを確認します。有効なら手順6へ。無効なら手順3へ。

#. ログイン試行が無効でした。この試行のユーザー名とIPアドレスをキャッシュに追加します。
これが制限を超えた場合、ブロックリストに追加し、手順5へ。制限を超えていない場合は手順4へ。

#. ログインは無効でしたが、制限を超えていません。ログイン画面に戻して再試行させます。

#. ユーザーがブロックされました:ブロックページに遷移させ、ブロックされていることと、ブロック解除の推定時間を伝えます。

#. ログインが有効です。失敗したログイン試行をリセットし、目的のページに転送します。

キャッシュバックエンド

Defenderはキャッシュを使用して失敗した試行を保存します。

キャッシュキー


カウンター:

  • prefix:failed:ip:[ip] (カウント, TTL)
  • prefix:failed:username:[username] (カウント, TTL)

ブール値(存在すればブロック中):

  • prefix:blocked:ip:[ip] (true, TTL)
  • prefix:blocked:username:[username] (true, TTL)

django-defenderのカスタマイズ

django-defender を少しカスタマイズするためのオプションがいくつか用意されています。
これらは settings.py で定義してください。

  • DEFENDER_LOGIN_FAILURE_LIMIT\ : 整数:不正ログインのレコードが作成されるまでに許可されるログイン試行回数。 [デフォルト: 3\ ]

  • DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME\ : 整数:ユーザー名ごとに不正ログインのレコードが作成されるまでに許可されるログイン試行回数。 [デフォルト: DEFENDER_LOGIN_FAILURE_LIMIT\ ]

  • DEFENDER_LOGIN_FAILURE_LIMIT_IP\ : 整数:IPごとに不正ログインのレコードが作成されるまでに許可されるログイン試行回数。 [デフォルト: DEFENDER_LOGIN_FAILURE_LIMIT\ ]

  • DEFENDER_BEHIND_REVERSE_PROXY\ : ブール値:defenderがリバースプロキシの背後にあるかどうか。 [デフォルト: False\ ]

  • DEFENDER_REVERSE_PROXY_HEADER\ : 文字列:リバースプロキシのIPアドレスを含むHTTPヘッダーの名前。 [デフォルト: HTTP_X_FORWARDED_FOR\ ]

  • DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME\ : ブール値:IPとユーザー名の組み合わせでユーザーをロックアウトします。これにより、同じIPアドレスからアクセスする他のユーザーがアプリケーション全体にアクセスできなくなるのを防ぎます。 [デフォルト: \ ]

DEFENDER_ATTEMPT_COOLOFF_TIME と DEFENDER_LOCKOUT_COOLOFF_TIME を使用する根拠


ほとんどのユースケースでは DEFENDER_COOLOFF_TIME だけで十分ですが、高セキュリティ環境などの特定のシナリオで defender を使用する場合、開発者はロックアウト対象とみなされる間の無効なログイン試行が「記憶」される期間と、実際にロックアウトキーがシステムからロックアウトされる時間をより細かく制御したい場合があります。DEFENDER_ATTEMPT_COOLOFF_TIME と DEFENDER_LOCKOUT_COOLOFF_TIME は、このような細かい設定を可能にします。

また、低セキュリティかつ低規模の例として、高校のウェブサイトを考えてみましょう。このようなウェブサイトは学校のコンピュータで運用され、学校のITスタッフや(幸運にもいれば)コンピュータサイエンスの教師が管理しているかもしれません。このシナリオでは、認証なしでアクセスできるウェブサイトの部分も多いでしょうが、ログインすると学生の名前、メールアドレス、成績、授業スケジュールなどの比較的機密性の高い情報にアクセスできるようになります。最後に、アカウントにメールがリンクされているため、パスワードリセット機能があり、リセットが完了するとアカウントのブロックが解除されると仮定します。このような場合、アプリケーションは単にDoS攻撃に対する保護を目的としているため、失敗ログインを長時間記憶する必要はないでしょう。これは、DEFENDER_ATTEMPT_COOLOFF_TIME を30秒といった低い値に保ち、DEFENDER_LOCKOUT_COOLOFF_TIME を600秒といったはるかに高い値に設定することで実現できます。DEFENDER_ATTEMPT_COOLOFF_TIME を低く保ち、DEFENDER_LOCKOUT_COOLOFF_TIME を高く設定して悪意のあるアクターを長時間ロックアウトすることで、高速なブルートフォース攻撃は防御され、サーバーのキャッシュ領域を他のデータのために確保できます。また、前述のパスワードリセット機能を提供することで、仮想的な管理者は実際のユーザーのブロック解除への関与を制限しつつ、ウェブサイトのアクセシビリティを維持できます。

前の例はやや作为てですが、これらの設定の真価は以下の説明と例で示されます。

DEFENDER_STORE_ACCESS_ATTEMPTS が True の場合、DEFENDER_LOCKOUT_COOLOFF_TIME を整数のリストとして設定することもできます。リストとして設定した場合、設定されたロックアウトキーに対する過去の失敗ログイン試行回数を DEFENDER_LOGIN_FAILURE_LIMIT で割り、DEFENDER_ACCESS_ATTEMPT_EXPIRATION で定義された期間内の失敗ログイン回数を意図的に過大評価した数値を生成します。これは、失敗ログイン試行間の時間を考慮しないため、過大評価になります。これは厳しいように思えるかもしれませんが、特定のシナリオでは、低速攻撃に対する追加の保護が、実際のユーザーに生じる可能性のある不便さを上回る価値があります。

そのような例の一つとして、ユーザーの機密情報(例えば個人の財務記録)を保存する、公開Webアプリケーションが考えられます。アプリケーションとそのデータは最小限の中断でアクセス可能であるべきですが、セキュリティは不可欠であるため、ある程度の遅延は許容されます。このような状況では、単に DEFENDER_COOLOFF_TIME を非常に大きな整数または0に設定して最大限の保護をしたいと考えるかもしれません。しかし、これでは実際のユーザーがシステムからロックアウトされた場合、管理者が手動でブロックを解除する必要があり、面倒でコストがかかります。DEFENDER_ATTEMPT_COOLOFF_TIME を十分に大きな数値(例えば600)に設定し、DEFENDER_LOCKOUT_COOLOFF_TIME を増加する整数のリスト(例:[60, 120, 300, 600, 0])に設定することで、単に DEFENDER_COOLOFF_TIME を600に設定した場合と同等の保護を実現しつつ、ユーザーへの影響を大幅に軽減できます。

その他の認証方法への適応

defender は「Django認証システム」以外の認証にも使用できます。
例えば、django-rest-framework の認証をブルートフォース攻撃から保護する必要がある場合、カスタム認証メソッドを実装できます。

以下は djangorestframework.BasicAuthentication に基づいた BasicAuthenticationDefender クラスのサンプルです:

.. code-block:: python

import base64 import binascii

from django.utils.translation import gettext_lazy as _

from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework.authentication import ( BasicAuthentication, get_authorization_header, )

from defender import utils from defender import config

class BasicAuthenticationDefender(BasicAuthentication):def get_username_from_request(self, request): auth = get_authorization_header(request).split() return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0]

root@kitploit:~
   def authenticate(self, request):
       auth = get_authorization_header(request).split()

       if not auth or auth[0].lower() != b'basic':
           return None

       if len(auth) == 1:
           msg = _('Invalid basic header. No credentials provided.')
           raise exceptions.AuthenticationFailed(msg)
       elif len(auth) > 2:
           msg = _('Invalid basic header. Credentials string should not contain spaces.')
           raise exceptions.AuthenticationFailed(msg)

       if utils.is_already_locked(request, get_username=self.get_username_from_request):
           detail = "You have attempted to login {failure_limit} times, with no success." \
                    "Your account is locked for {cooloff_time_seconds} seconds" \
                    "".format(
                       failure_limit=config.FAILURE_LIMIT,
                       cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[
                          defender_utils.get_lockout_cooloff_time(username=self.get_username_from_request(request))
                       ]
                    )
           raise exceptions.AuthenticationFailed(_(detail))

       try:
           auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
       except (TypeError, UnicodeDecodeError, binascii.Error):
           msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
           raise exceptions.AuthenticationFailed(msg)

       userid, password = auth_parts[0], auth_parts[2]
       login_unsuccessful = False
       login_exception = None
       try:
           response = self.authenticate_credentials(userid, password)
       except exceptions.AuthenticationFailed as e:
           login_unsuccessful = True
           login_exception = e

       utils.add_login_attempt_to_db(request,
                                     login_valid=not login_unsuccessful,
                                     get_username=self.get_username_from_request)
       # add the failed attempt to Redis in case of a failed login or resets the attempt count in case of success
       utils.check_request(request,
                           login_unsuccessful=login_unsuccessful,
                           get_username=self.get_username_from_request)
       if login_unsuccessful:
           raise login_exception

       return response

動作させるには、BasicAuthenticationDefender を settings.py の DEFAULT_AUTHENTICATION_CLASSES に、他のすべての認証メソッドよりも上に追加します。

他の認証方法への適用 :- djangorestframework 内の django-rest-auth

defender は、django-rest-framework と django-rest-auth の組み合わせで組み込むことができ、ユーザー認証に使用できます。

参考


  • https://www.django-rest-framework.org/
  • https://django-rest-auth.readthedocs.io/en/latest/

以下は、django-rest-auth ライブラリを使用してユーザー認証を行う rest_framework.authentication.TokenAuthentication に基づいた BasicAuthenticationDefender クラスのサンプルです。

.. code-block:: python

import base64 import binascii

from django.conf import settings from django.contrib.auth import get_user_model, authenticate from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm from django.contrib.auth.tokens import default_token_generator from django.utils.http import urlsafe_base64_decode as uid_decoder from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_str from rest_framework import serializers, exceptions, HTTP_HEADER_ENCODING from rest_framework.exceptions import ValidationError from defender import utils as defender_utils from defender import config from rest_framework.authentication import ( get_authorization_header, )

Get the UserModel

UserModel = get_user_model()

class BasicAuthenticationDefender(serializers.Serializer):

root@kitploit:~
  username = serializers.CharField(required=False, allow_blank=True)
  email = serializers.EmailField(required=False, allow_blank=True)
  password = serializers.CharField(style={'input_type': 'password'})

  def authenticate(self, **kwargs):
    request = self.context['request']

    if hasattr(settings, 'ACCOUNT_AUTHENTICATION_METHOD'):
        login_field = settings.ACCOUNT_AUTHENTICATION_METHOD
    else:
        login_field = 'username'
    userid = self.username_from_request(request, login_field)

    if defender_utils.is_already_locked(request, username=userid):
        detail = "You have attempted to login {failure_limit} times with no success. "
                 .format(
                     failure_limit=config.FAILURE_LIMIT,
                     cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[defender_utils.get_lockout_cooloff_time(username=userid)]
                 )
        raise exceptions.AuthenticationFailed(_(detail))

    login_unsuccessful = False
    login_exception = None
    try:
        response = authenticate(request, **kwargs)
        if response == None:
            login_unsuccessful = True
            msg = _('Unable to log in with provided credentials.')
            # raise exceptions.ValidationError(msg)
            login_exception = exceptions.ValidationError(msg)
    except exceptions.AuthenticationFailed as e:
        login_unsuccessful = True
        login_exception = e

    defender_utils.add_login_attempt_to_db(request,
                                           login_valid=not login_unsuccessful,
                                           username=userid)

    user_not_blocked = defender_utils.check_request(request,
                                                    login_unsuccessful=login_unsuccessful,
                                                    username=userid)
    if user_not_blocked and not login_unsuccessful:
        return response

    raise login_exception

  def _validate_email(self, email, password):
    user = None

    if email and password:
        user = self.authenticate(email=email, password=password)
    else:
        msg = _('Must include "email" and "password".')
        raise exceptions.ValidationError(msg)

    return user

  def _validate_username(self, username, password):
    user = None

    if username and password:
        user = self.authenticate(username=username, password=password)
    else:
        msg = _('Must include "username" and "password".')
        raise exceptions.ValidationError(msg)

    return user

  def _validate_username_email(self, username, email, password):
    user = None

    if email and password:
        user = self.authenticate(email=email, password=password)
    elif username and password:
        user = self.authenticate(username=username, password=password)
    else:
        msg = _('Must include either "username" or "email" and "password".')
        raise exceptions.ValidationError(msg)

    return user

  def validate(self, attrs):
    username = attrs.get('username')
    email = attrs.get('email')
    password = attrs.get('password')

    user = None

    if 'allauth' in settings.INSTALLED_APPS:
        from allauth.account import app_settings

        # Authentication through email
        if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:
            user = self._validate_email(email, password)

        # Authentication through username
        elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME:
            user = self._validate_username(username, password)

        # Authentication through either username or email
        else:
            user = self._validate_username_email(username, email, password)

    else:
        # Authentication without using allauth
        if email:
            try:
                username = UserModel.objects.get(
                    email__iexact=email).username()
            except UserModel.DoesNotExist:
                pass

        if username:
            user = self._validate_username_email(username, '', password)

    # Did we get back an active user?
    if user:
        if not user.is_active:
            msg = _('User account is disabled.')
            raise exceptions.ValidationError(msg)
    else:
        msg = _('Unable to log in with provided credentials.')
        raise exceptions.ValidationError(msg)

    # If required, is the email verified?
    if 'rest_auth.registration' in settings.INSTALLED_APPS:
        from allauth.account import app_settings
        if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:
            email_address = user.emailaddress_set.get(email=user.email)
            if not email_address.verified:
                raise serializers.ValidationError(
                    _('E-mail is not verified.'))

    attrs['user'] = user
    return attrs

  def username_from_request(self, request, login_field):
    user_data = request._data
    return user_data[login_field]

動作させるには、BasicAuthenticationDefender を settings.py の REST_AUTH_SERIALIZERS 辞書に、キー LOGIN_SERIALIZER の下に追加します。 例えば、settings.py に以下の行を追加します:

.. code-block:: python

REST_AUTH_SERIALIZERS = { 'LOGIN_SERIALIZER': '.BasicAuthenticationDefender', }

パスワードリセットフォームへの適用

defender は、Django の PasswordResetView に適用して、過剰な送信を防止できます。

Django の組み込み LoginView、PasswordResetView、PasswordResetConfirmView をサブクラス化する新しいビューを作成し、それらのビューを Django の組み込みの代わりに urls.py で使用する必要があります。

ビューは、パスワードリセットビューで送信されたメールアドレスに基づいてブロックします。これはデフォルトの実装(ユーザー名を使用)とは異なるため、サインイン時およびパスワードリセット完了後に後片付けを注意深く行う必要があります。

.. code-block:: python

root@kitploit:~
from defender import utils as def_utils
from django.contrib.auth import views as auth_views

class UserSignIn(auth_views.LoginView):
    def form_valid(self, form):
        """Force clear all the cached Defender statues for the authenticated user’s email address."""
        super_valid = super().form_valid(form)
        def_utils.check_request(self.request, False, username=form.get_user().email)
        return super_valid

class PasswordResetBruteForceProtectedView(auth_views.PasswordResetView):
    def get(self, request, *args, **kwargs):
        """Confirm the user isn’t already blocked by IP before showing the password reset view."""
        if def_utils.is_already_locked(request):
            return def_utils.lockout_response(request)
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        """
        Confirm the user isn’t already blocked by IP before allowing form POST.

        Also, force log this form POST as a single entry in the Defender cache, against the submitted email address.
        """
        if def_utils.is_already_locked(request):
            return def_utils.lockout_response(request)
        def_utils.check_request(
            request, login_unsuccessful=True, username=request.POST.get("email")
        )
        return super().post(request, *args, **kwargs)


class PasswordResetConfirmBruceForceProtectedView(auth_views.PasswordResetConfirmView):
    def get(self, request, *args, **kwargs):
        """Confirm the user isn’t already blocked by IP before showing the password confirm view."""
        if def_utils.is_already_locked(request):
            return def_utils.lockout_response(request)
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        """Confirm the user isn’t already blocked by IP before allowing form POST for the password change confirmation."""
        if def_utils.is_already_locked(request):
            return def_utils.lockout_response(request)
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        """Force clear all the cached Defender statues for the user’s email address after successfully changing their password."""
        super_valid = super().form_valid(form)
        def_utils.check_request(
            self.request, login_unsuccessful=False, username=self.user.email
        )
        return super_valid

Django シグナル

django-defender は、ユーザー名または IP アドレスをブロックするときにシグナルを送信します。シグナルレシーバー関数を設定するには:

.. code-block:: python

from django.dispatch import receiver

from defender import signals

@receiver(signals.username_block) def username_blocked(username, **kwargs): print("%s was blocked!" % username)

@receiver(signals.ip_block) def ip_blocked(ip_address, **kwargs): print("%s was blocked!" % ip_address)

テストの実行

リポジトリをクローンし、Django をインストールした後、次のようにテストを実行できます:

.. code-block:: bash

PYTHONPATH=$PYTHONPATH:$PWD django-admin test defender --settings=defender.test_settings

コードカバレッジ付き:

.. code-block:: bash

PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin) test defender --settings=defender.test_settings

リリース

#. python setup.py sdist #. twine upload dist/*

ツールをダウンロード
False
  • DEFENDER_DISABLE_IP_LOCKOUT\ : ブール値:Trueの場合、ユーザーのIPアドレスをロックアウトせず、ユーザー名のみロックアウトします。 [デフォルト: False]

  • DEFENDER_DISABLE_USERNAME_LOCKOUT\ : ブール値:Trueの場合、ユーザー名をロックアウトせず、IPアドレスのみロックアウトします。 [デフォルト: False]

  • DEFENDER_COOLOFF_TIME\ : 整数:設定すると、一定期間の非アクティブ後、古い失敗ログイン試行とユーザー名/IPロックアウトが忘れられます。整数値は秒数と解釈されます。0の場合、失敗ログイン試行もユーザー名/IPロックも期限切れになりません。 [デフォルト: 300\ ]

  • DEFENDER_ATTEMPT_COOLOFF_TIME\ : 整数:設定すると、DEFENDER_COOLOFF_TIME で設定された失敗ログイン試行の忘却期間を上書きします。整数値は秒数と解釈されます。0の場合、失敗ログイン試行は期限切れになりません。 [デフォルト: DEFENDER_COOLOFF_TIME\ ]

  • DEFENDER_LOCKOUT_COOLOFF_TIME\ : 整数またはリスト:設定すると、DEFENDER_COOLOFF_TIME で設定されたユーザー名/IPロックアウトの忘却期間を上書きします。整数値は秒数と解釈されます。整数のリストは、過去の DEFENDER_ACCESS_ATTEMPT_EXPIRATION 時間内のロックアウト回数(上限あり)に応じて秒数が異なることを意味します。プロパティが0または[]の場合、ユーザー名/IPロックアウトは期限切れになりません。 [デフォルト: DEFENDER_COOLOFF_TIME\ ]

  • DEFENDER_LOCKOUT_TEMPLATE\ : 文字列: [デフォルト: None\ ] 設定すると、ユーザーがロックアウトされたときにレンダリングするテンプレートを指定します。テンプレートには次のコンテキスト変数が渡されます:

    • cooloff_time_seconds\ : クールオフ時間(秒)
    • cooloff_time_minutes\ : クールオフ時間(分)
    • failure_limit\ : ブロックされるまでの失敗許容回数。
  • DEFENDER_USERNAME_FORM_FIELD\ : 文字列:ユーザーのユーザー名を含むフォームフィールドの名前。 [デフォルト: username\ ]

  • DEFENDER_CACHE_PREFIX\ : 文字列:defenderキーのキャッシュプレフィックス。 [デフォルト: defender\ ]

  • DEFENDER_LOCKOUT_URL\ : 文字列:ロックアウトされた場合にリダイレクトするURL。

  • DEFENDER_REDIS_URL\ : 文字列:defenderのRedis URL。 [デフォルト: redis://localhost:6379/0\ ] (パスワード付きの例: redis://:mypassword@localhost:6379/0\ )

  • DEFENDER_REDIS_PASSWORD_QUOTE\ : ブール値:Redisパスワードに特殊文字(@など)が含まれる場合、urllib.parse.quote("password!@#") でパスワードをクォートし、Trueに設定します。 [デフォルト: False\ ]

  • DEFENDER_REDIS_NAME\ : 文字列:Django設定の CACHES 内のキャッシュ名(例:"default")。設定すると、DEFENDER_REDIS_URL は無視されます。 [デフォルト: None\ ]

  • DEFENDER_STORE_ACCESS_ATTEMPTS\ : ブール値:ログイン試行をデータベースに保存する場合はTrue。Falseの場合は保存しません。 [デフォルト: True\ ]

  • DEFENDER_USE_CELERY\ : ブール値:Celeryを使用してログイン試行をデータベースに保存する場合はTrue。Falseの場合はインラインで保存します。 [デフォルト: False\ ]

  • DEFENDER_ACCESS_ATTEMPT_EXPIRATION\ : 整数:管理コマンドでクリーンアップされるまで、アクセス試行レコードをデータベースに保持する時間(時間単位)。 [デフォルト: 24\ ]

  • DEFENDER_GET_USERNAME_FROM_REQUEST_PATH\ : 文字列:リクエストからユーザー名にアクセスする関数のインポートパス。リクエストからユーザー名にアクセスして処理するカスタム関数を使用する場合に指定します。 [デフォルト: defender.utils.username_from_request\ ]