عنوان:

‫طراحی یک ماژول IpBlocker در ASP.NET MVC


نویسنده: احمد نواصری
تاریخ: ۱۳۹۵/۰۱/۰۴ ۲۱:۳۰
آدرس: www.dntips.ir
همانطور که میدانید وب سایت‌های اینترنتی در معرض انواع و اقسام حملات قرار دارند و یکی از این حملات 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 که زحمت کشیدید مد نظرم بود که آیا چنین قابلیتی دارد یا خیر؟
        • وحید نصیری در ۱۳۹۷/۰۸/۱۴ ۱۳:۲۶
          خیر.