طراحی یک ماژول IpBlocker در ASP.NET MVC
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: دو دقیقه

همانطور که میدانید وب سایت‌های اینترنتی در معرض انواع و اقسام حملات قرار دارند و یکی از این حملات Dos است. در این نوشتار میخواهیم تکه کدی را ارائه دهیم، تا این نوع حملات را دفع نماید. همانطور که میدانید یک درخواست Http باید از ماژول‌های مختلفی عبور نماید تا به یک Http Handler برسد.
ابتدا باید یک Enum تعریف کنیم تا نوع درخواست کاربر را مشخص کند. مثلا 100 درخواست ابتدایی را به عنوان FirstVisite در نظر گرفته و اگر تعداد درخواستها از 100 گذشت، در دسته Revisit قرار میگیرند و ... . البته این بستگی به شما دارد که مقادیر را چقدر در نظر بگیرید؛ اما دقت کنید.
private enum VisiteType
        {
            FirstVisite = 2,
            Reviste = 4,
        }
در مرحله بعدی باید آدرس Ip بیننده سایت را بدست آورده وچک کنیم که این آدرس Ip در کش موجود هست یا خیر. اگر موجود بود مقدار متغیر hits را افزایش میدهیم و چک میکنیم که متغیر با کدام یک از مقادیر Enum برابری میکند و آن را در کش سیستم ذخیره میکنیم.
if (httpContext.Cache[ipAddress] != null)
                {
                    hits++;

                    if (hits == (int)VisiteType.FirstVisite)
                    {
                        httpContext.Cache.Insert(key: ipAddress,
                           value: hits,
                           dependencies: null,
                           absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                           slidingExpiration: Cache.NoSlidingExpiration);
                        return;
                    }
در تکه کد بالا چون درخواست به حد معین و معقولی رسیده است، آدرس بعد از یک ثانیه از کش حذف میشود. اما اگر درخواستهای غیرمعقولی به سرور ارسال شود، باید پاسخ را قطع و آدرس Ip را در کش ذخیره و این آدرس Ip باید به مدت معینی در کش مانده و بلاک شود.
using System;
using System.Net;
using System.Web;
using System.Web.Caching;

namespace IpBlocker
{
    public class IpBlocker : IHttpModule
    {
        private int hits = 0;
        /// <summary>
        /// Define enum to specify visite type
        /// </summary>
        private enum VisiteType
        {
            FirstVisite = 2,
            Reviste = 4,
        }
        public void Init(HttpApplication context)
        {
            context.BeginRequest += OnBeginRequest;
        }
        public void Dispose()
        {

        }

        public void OnBeginRequest(object sender, EventArgs e)
        {
            var httpApplication = sender as HttpApplication;
            var httpContext = httpApplication?.Context;
            ProcessRequest(httpApplication, httpContext);
        }

        private void ProcessRequest(HttpApplication application, HttpContext httpContext)
        {
            //Checke if browser is a search engine web crawler
            if (httpContext.Request.Browser.Crawler)
                return;

            var ipAddress = application.Context.Request.UserHostAddress;

            if (httpContext.Cache[ipAddress] == null)
            {
                // Reset hits for new request after blocking ip
                if (hits > 0)
                    hits = 0;

                hits++;

                httpContext.Cache.Insert(key: ipAddress,
                   value: hits,
                   dependencies: null,
                   absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                   slidingExpiration: Cache.NoSlidingExpiration);

                return;
            }          

            else
            {
                if (httpContext.Cache[ipAddress] != null)
                {
                    hits++;

                    if (hits == (int)VisiteType.FirstVisite)
                    {
                        httpContext.Cache.Insert(key: ipAddress,
                           value: hits,
                           dependencies: null,
                           absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                           slidingExpiration: Cache.NoSlidingExpiration);
                        return;
                    }

                    if (hits == (int)VisiteType.Reviste)
                    {

                        httpContext.Cache.Insert(key: ipAddress,
                           value: hits,
                           dependencies: null,
                           absoluteExpiration: DateTime.UtcNow.AddSeconds(1),
                           slidingExpiration: Cache.NoSlidingExpiration);
                        return;
                    }
                    if (hits > (int)VisiteType.Reviste)
                    {
                        httpContext.Cache.Insert(key: ipAddress,
                            value: hits,
                            dependencies: null,
                            absoluteExpiration: DateTime.UtcNow.AddMinutes(1),
                            slidingExpiration: Cache.NoSlidingExpiration);

                        httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                        httpContext.Response.SuppressContent = true;
                        httpContext.Response.End();
                    }
                }
            }
        }
    }
}
در کد بالا محدودیت زمانی یک دقیقه در نظر گرفته شده است.

  • #
    ‫۶ سال و ۴ ماه قبل، دوشنبه ۱۰ اردیبهشت ۱۳۹۷، ساعت ۱۳:۴۹
    معادل این مطلب در برنامه‌های ASP.NET Core

    ابتدا بسته‌ی نیوگت DNTCommon.Web.Core را نصب کنید:
    PM> Install-Package DNTCommon.Web.Core
    سپس میان افزار AntiDos آن‌را به صورت زیر می‌توانید استفاده و تنظیم کنید:
    الف) آن‌را پیش از هر میان‌افزار دیگری ثبت کنید:
    public void Configure(IApplicationBuilder app)
    {
      app.UseAntiDos();
    ب) تنظیمات مخصوص آن‌را به فایل appsettings.json اضافه کنید.
    ج) این تنظیمات را به صورت زیر به برنامه معرفی کنید:
    public void ConfigureServices(IServiceCollection services)
    {
       services.Configure<AntiDosConfig>(options => Configuration.GetSection("AntiDosConfig").Bind(options));
    این میان‌افزار هم یک فایروال است که می‌تواند یک رنج‌آی‌پی را ببندد و هم می‌تواند کلاینت‌ها را بر اساس user agent و همچنین هدرهای خاصی فیلتر کند. به علاوه در آن می‌توانید تنظیم کنید که یک کاربر در هر دقیقه چندبار می‌تواند درخواستی را به سمت سایت ارسال کند و پس از آن تا مدتی دسترسی آن به سایت قطع شود. 
    • #
      ‫۶ سال قبل، پنجشنبه ۲۲ شهریور ۱۳۹۷، ساعت ۲۲:۲۷
      با سلام 
      امکان چک درخواست‌ها بر ثانیه  هم موجود است؟ 
      و چگونه به UserAgent دسترسی داریم که بتوان اطلاعات کاربر رو در بانک لاگ کرد.
      • #
        ‫۶ سال قبل، پنجشنبه ۲۲ شهریور ۱۳۹۷، ساعت ۲۳:۰۸
        - خیر. تنظیم این اعداد به مقادیر کوچک، کاربران عادی را هم از انجام کارهای ساده عاجز می‌کند.
        - اطلاعات IPهای بسته شده توسط خود این ماژول لاگ می‌شود. اگر سیستم لاگر مبتنی بر دیتابیس را به برنامه اضافه کنید، این لاگ‌ها به صورت خودکار در بانک اطلاعاتی ذخیره خواهند شد. در آنجا آی‌دی کاربر یا هر اطلاعات دیگری را هم خودتان به ازای هر لاگ ارائه شده، ذخیره کنید. البته اگر از Shadow properties و یا سیستم Tracking استفاده می‌کنید، لاگر مبتنی بر دیتابیس  اطلاعات کاملی را از کاربر جاری ذخیره می‌کند و نیازی به کدهای اضافه‌تری ندارد.
        • #
          ‫۶ سال قبل، جمعه ۲۳ شهریور ۱۳۹۷، ساعت ۰۰:۱۳
          ممنون
          پیاده سازی همیچین ویژگی با Action filter‌ها قابل انجام است؟‌
          برای مثال اگر برای اکشن خواستی بیش از 50 درخواست در ثانیه بود بشود اطلاعات کلاینت مربوط رو بدست آورد.   
          • #
            ‫۶ سال قبل، جمعه ۲۳ شهریور ۱۳۹۷، ساعت ۰۰:۱۷
            استفاده از Action filter‌ها برای این نوع کارها بیشتر در ASP.NET MVC 5.x مرسوم بودند. در اینجا یک میان‌افزار برای آن تهیه شده که اطلاعات کامل کلاینت بسته شده را در سیستم استاندارد و توکار Logging ثبت می‌کند. پس از آن اگر از پروایدر Console سیستم Logging استفاده کنید، نتیجه را در صفحه‌ی Console مشاهده خواهید کرد و اگر پروایدر و لاگر مبتنی بر دیتابیس را برای آن تهیه کنید، نتایج را در بانک اطلاعاتی لاگ می‌کند.
    • #
      ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۱۵:۵۵
      زمانیکه از این میان افزار در یک پروژه انگولاری استفاده میکنم کانفلیکت ایجاد میشه:

      • #
        ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۱۶:۳۶
        شماره کد 409 در اینجا یعنی سقف تنظیم شده پر شده و کلاینت بسته شده.
        • #
          ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۱۶:۵۴
          سقف درخواست‌ها را هم تغییر دادم اما باز هم 409 صادر میشه:
           "DurationMin": 1,
           "AllowedRequests": 100,

          • #
            ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۱۷:۲۹
            اطلاعات IPهای بسته شده توسط خود این ماژول لاگ می‌شود. چه چیزی را در لاگ مشاهده می‌کنید؟ یک قسمت آن reason هست یا دلیل بسته شدن. این لاگ‌ها را در کنسول می‌توانید مشاهده کنید. برنامه‌ی ASP.NET Core خود را با دستور dotnet run اجرا و لاگ‌های کنسول آن‌را بررسی کنید.
            • #
              ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۱۸:۵۴
              warn: DNTCommon.Web.Core.AntiDosFirewall[0]
                    Banned IP: $10.45.142.75, UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36. RequestsCount: 21, ExpiresAt: 9/18/18 9:42:17 AM +00:00,  Reason: IsDosAttack
              در این لاگ سقف درخواست‌ها را روی 20 تنظیم کردم که سقف درخواست به حداکثر رسیده و IsDosAttack فعال شده. اما مسئله اینجاس که در لود اولیه برنامه حدود 11 درخواست به سرور ارسال می‌شود و وقتی کاربر بخش‌های مختلف سایت را درخواست کند سقف درخواست‌ها به حداکثر میرسه و IP بسته میشه. در اینجا تمامی resource‌های درخواست شده از سرور محاسبه می‌شود. راهی وجود ندارد که این میان افزار را فقط روی اکشن متدها اعمال کرد؟ 
              • #
                ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۲۰:۵۸
                درستش همین حالت فعلی هست. چون درخواست تصاویر و یا اسکریپت‌ها و CSSها هم درخواست واقعی به سرور هستند و باید در محاسبات AntiDos لحاظ شوند. عدد 20 هم کم است. عدد را روی 500 یا 1000 قرار دهید. کسانیکه شروع می‌کنند به حمله، خیلی سریع این 1000 تا را رد می‌کنند؛ در چند ثانیه فقط. هدف ماژول AntiDos این نوع حملات است و نه تداخل با کار عادی کاربران و یا حتی موتورهای جستجو را هم باید مدنظر داشته باشید.
          • #
            ‫۶ سال قبل، سه‌شنبه ۲۷ شهریور ۱۳۹۷، ساعت ۱۸:۰۱
            نگارش 1.3.1 را آزمایش کنید. بررسی «"NullIPv6 = "::1 » اضافه شد.
    • #
      ‫۴ سال و ۱۱ ماه قبل، جمعه ۱۹ مهر ۱۳۹۸، ساعت ۲۲:۱۷
      با سلام
      بعد از بروزرسانی به DotNetCore 3 خیلی سریع این خطا را در لاگ میدهد:
      Banned IP: xx.xx.xx.xx, UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0. RequestsCount: 1000, ExpiresAt: 10/12/2019 1:42:46 AM +00:00,  Reason: BadBotRequestHeader: jaguar.
      دلیلش چه چیزی میتونه باشه؟ قبل از بروز رسانی مشکلی نداشتیم، ولی الان فوری بلاک میشود!
      • #
        ‫۴ سال و ۱۱ ماه قبل، جمعه ۱۹ مهر ۱۳۹۸، ساعت ۲۲:۴۸
        jaguar ای را که عنوان کرده، از لیست حذف کنید.
        • #
          ‫۴ سال و ۱۱ ماه قبل، جمعه ۱۹ مهر ۱۳۹۸، ساعت ۲۳:۳۱
          jaguar  چی هست؟
          من هدر‌ها رو چک کردم، هدری از سمت مرورگر من با این عنوان ارسال نشده! با سه سیستم مختلف در سه منطقه مختلف چک کردم
          • #
            ‫۴ سال و ۱۱ ماه قبل، جمعه ۱۹ مهر ۱۳۹۸، ساعت ۲۳:۴۵
            یکی از چندین RequestHeaders دریافتی شما است. جائی مزاحمت ایجاد کرده بوده، در این لیست هست. اگر نیازی نیست، همانطور که عنوان شد از این لیست حذفش کنید تا پیام BadBotRequestHeader ای را که گرفتید، مشاهده نکنید.
  • #
    ‫۵ سال و ۱۰ ماه قبل، دوشنبه ۱۴ آبان ۱۳۹۷، ساعت ۱۱:۱۸
    با سلام؛ من دنبال راهی هستم که بتوانم تعداد درخواست‌های روی یک اکشن اگر از تعدادی بیشتر شود  اون ip مسدود شود چنین راهی وجود دارد؟
      • #
        ‫۵ سال و ۱۰ ماه قبل، دوشنبه ۱۴ آبان ۱۳۹۷، ساعت ۱۶:۲۲
        با تشکر. با استفاده از میان افزار AntiDos  در mvc core که زحمت کشیدید مد نظرم بود که آیا چنین قابلیتی دارد یا خیر؟
        • #
          ‫۵ سال و ۱۰ ماه قبل، دوشنبه ۱۴ آبان ۱۳۹۷، ساعت ۱۶:۵۶
          خیر.