如何在ASP.NET中延迟/限制登录尝试?

9

我正在尝试在我的ASP.NET Web项目上进行一些非常简单的请求限制。目前我对于全局限制针对DOS攻击并不感兴趣,但是希望人为地延迟所有登录尝试的响应,以使字典攻击变得更加困难(就像Jeff Atwood在这里中概述的那样)。

你会如何实现它呢?做法似乎很简单-只需调用

Thread.Sleep();

请求过程中的某个位置。有建议吗?:)

6个回答

4

我和你一样,想要提高登录页面(和密码重置页面)的安全性。我将在我的项目中实现这个功能,并与你分享我的经历。

需求

我的需求如下:

  • 不要因为有人试图入侵而阻止单个用户的登录
  • 我的用户名很容易被猜到,因为它们遵循某种模式(我不喜欢通过模糊来保证安全性)
  • 不要通过休眠太多请求来浪费服务器资源,队列最终会溢出并且请求会开始超时
  • 为大多数用户提供快速服务,99% 的时间内可以完成
  • 消除登录页面上的暴力破解攻击
  • 处理分布式攻击
  • 需要合理地支持多线程

计划

所以我们将有一个失败尝试列表和其时间戳。每次进行登录尝试时,我们都会检查这个列表,如果有更多的失败尝试,登录时间就会更长。每次我们都会根据时间戳删除旧条目。超过某个阈值后,将不允许登录,并立即失败所有登录请求(攻击紧急关闭)。

我们不会止步于自动保护。在紧急关闭情况下,应向管理员发送通知,以便进行调查并采取修复措施。我们的日志应包含失败尝试的坚实记录,包括时间、用户名和源 IP 地址,以供调查。

计划将此实现为静态声明队列,其中失败尝试进入队列,旧条目出队列。队列长度是我们的严重程度指标。当我准备好代码后,我会更新答案。我可能还会包括 Keltex 的建议 - 快速释放响应并使用另一个请求完成登录。

更新:有两个缺失:

  1. 响应重定向到等待页面,以免阻塞请求队列,这显然是一个大问题。我们需要给用户一个令牌,以便稍后使用另一个请求进行检查。这可能是另一个安全漏洞,因此我们需要非常谨慎地处理它。或者只需在 Action 方法中删除 Thread.Sleap(xxx) :)
  2. IP,下次再说...

让我们看看最终能否解决这些问题...

已完成的工作

ASP.NET 页面

ASP.NET UI 页面应该尽量简化,然后我们可以像这样获取 Gate 的实例:

static private LoginGate Gate = SecurityDelayManager.Instance.GetGate<LoginGate>();

登录(或密码重置)尝试后,请调用以下函数:

SecurityDelayManager.Instance.Check(Gate, Gate.CreateLoginAttempt(success, UserName));

ASP.NET处理代码

LoginGate是在ASP.NET项目的AppCode中实现的,因此它可以访问所有前端好用的工具。它实现了IGate接口,后端SecurityDelayManager实例使用该接口。Action方法需要完成等待重定向。

public class LoginGate : SecurityDelayManager.IGate
{
    #region Static
    static Guid myID = new Guid("81e19a1d-a8ec-4476-a187-3130361a9006");
    static TimeSpan myTF = TimeSpan.FromHours(24);
    #endregion

    #region Private Types
    class LoginAttempt : Attempt { }
    class PasswordResetAttempt : Attempt { }
    class PasswordResetRequestAttempt : Attempt { }
    abstract class Attempt : SecurityDelayManager.IAttempt
    {
        public bool Successful { get; set; }
        public DateTime Time { get; set; }
        public String UserName { get; set; }

        public string SerializeForAuditLog()
        {
            return ToString();
        }
        public override string ToString()
        {
            return String.Format("{2} Successful:{0} @{1}", Successful, Time, GetType().Name);
        }
    }
    #endregion

    #region Attempt creation utility methods
    public SecurityDelayManager.IAttempt CreateLoginAttempt(bool success, string userName)
    {
        return new LoginAttempt() { Successful = success, UserName = userName, Time = DateTime.Now };
    }
    public SecurityDelayManager.IAttempt CreatePasswordResetAttempt(bool success, string userName)
    {
        return new PasswordResetAttempt() { Successful = success, UserName = userName, Time = DateTime.Now };
    }
    public SecurityDelayManager.IAttempt CreatePasswordResetRequestAttempt(bool success, string userName)
    {
        return new PasswordResetRequestAttempt() { Successful = success, UserName = userName, Time = DateTime.Now };
    }
    #endregion


    #region Implementation of SecurityDelayManager.IGate
    public Guid AccountID { get { return myID; } }
    public bool ConsiderSuccessfulAttemptsToo { get { return false; } }
    public TimeSpan SecurityTimeFrame { get { return myTF; } }


    public SecurityDelayManager.ActionResult Action(SecurityDelayManager.IAttempt attempt, int attemptsCount)
    {
        var delaySecs = Math.Pow(2, attemptsCount / 5);

        if (delaySecs > 30)
        {
            return SecurityDelayManager.ActionResult.Emergency;
        }
        else if (delaySecs < 3)
        {
            return SecurityDelayManager.ActionResult.NotDelayed;
        }
        else
        {
            // TODO: Implement the security delay logic
            return SecurityDelayManager.ActionResult.Delayed;
        }
    }
    #endregion

}

后端某种程度的线程安全管理

因此,我的核心库中的这个类将处理多线程尝试计数:

/// <summary>
/// Helps to count attempts and take action with some thread safety
/// </summary>
public sealed class SecurityDelayManager
{
    ILog log = LogManager.GetLogger(typeof(SecurityDelayManager).FullName + ".Log");
    ILog audit = LogManager.GetLogger(typeof(SecurityDelayManager).FullName + ".Audit");

    #region static
    static SecurityDelayManager me = new SecurityDelayManager();
    static Type igateType = typeof(IGate);
    public static SecurityDelayManager Instance { get { return me; } }
    #endregion

    #region Types
    public interface IAttempt
    {
        /// <summary>
        /// Is this a successful attempt?
        /// </summary>
        bool Successful { get; }

        /// <summary>
        /// When did this happen
        /// </summary>
        DateTime Time { get; }

        String SerializeForAuditLog();
    }

    /// <summary>
    /// Gate represents an entry point at wich an attempt was made
    /// </summary>
    public interface IGate
    {
        /// <summary>
        /// Uniquely identifies the gate
        /// </summary>
        Guid AccountID { get; }

        /// <summary>
        /// Besides unsuccessful attempts, successful attempts too introduce security delay
        /// </summary>
        bool ConsiderSuccessfulAttemptsToo { get; }

        TimeSpan SecurityTimeFrame { get; }

        ActionResult Action(IAttempt attempt, int attemptsCount);
    }

    public enum ActionResult { NotDelayed, Delayed, Emergency }

    public class SecurityActionEventArgs : EventArgs
    {
        public SecurityActionEventArgs(IGate gate, int attemptCount, IAttempt attempt, ActionResult result)
        {
            Gate = gate; AttemptCount = attemptCount; Attempt = attempt; Result = result;
        }
        public ActionResult Result { get; private set; }
        public IGate Gate { get; private set; }
        public IAttempt Attempt { get; private set; }
        public int AttemptCount { get; private set; }
    }
    #endregion

    #region Fields
    Dictionary<Guid, Queue<IAttempt>> attempts = new Dictionary<Guid, Queue<IAttempt>>();
    Dictionary<Type, IGate> gates = new Dictionary<Type, IGate>();
    #endregion

    #region Events
    public event EventHandler<SecurityActionEventArgs> SecurityAction;
    #endregion

    /// <summary>
    /// private (hidden) constructor, only static instance access (singleton)
    /// </summary> 
    private SecurityDelayManager() { }

    /// <summary>
    /// Look at the attempt and the history for a given gate, let the gate take action on the findings
    /// </summary>
    /// <param name="gate"></param>
    /// <param name="attempt"></param>
    public ActionResult Check(IGate gate, IAttempt attempt)
    {
        if (gate == null) throw new ArgumentException("gate");
        if (attempt == null) throw new ArgumentException("attempt");

        // get the input data befor we lock(queue)
        var cleanupTime = DateTime.Now.Subtract(gate.SecurityTimeFrame);
        var considerSuccessful = gate.ConsiderSuccessfulAttemptsToo;
        var attemptSuccessful = attempt.Successful;
        int attemptsCount; // = ?

        // not caring too much about threads here as risks are low
        Queue<IAttempt> queue = attempts.ContainsKey(gate.AccountID)
                                ? attempts[gate.AccountID]
                                : attempts[gate.AccountID] = new Queue<IAttempt>();

        // thread sensitive - keep it local and short
        lock (queue)
        {
            // maintenance first
            while (queue.Count != 0 && queue.Peek().Time < cleanupTime)
            {
                queue.Dequeue();
            }

            // enqueue attempt if necessary
            if (!attemptSuccessful || considerSuccessful)
            {
                queue.Enqueue(attempt);
            }

            // get the queue length
            attemptsCount = queue.Count;
        }

        // let the gate decide what now...
        var result = gate.Action(attempt, attemptsCount);

        // audit log
        switch (result)
        {
            case ActionResult.Emergency:
                audit.ErrorFormat("{0}: Emergency! Attempts count: {1}. {2}", gate, attemptsCount, attempt.SerializeForAuditLog());
                break;
            case ActionResult.Delayed:
                audit.WarnFormat("{0}: Delayed. Attempts count: {1}. {2}", gate, attemptsCount, attempt.SerializeForAuditLog());
                break;
            default:
                audit.DebugFormat("{0}: {3}. Attempts count: {1}. {2}", gate, attemptsCount, attempt.SerializeForAuditLog(), result);
                break;
        }

        // notification
        if (SecurityAction != null)
        {
            var ea = new SecurityActionEventArgs(gate, attemptsCount, attempt, result);
            SecurityAction(this, ea);
        }

        return result;
    }

    public void ResetAttempts()
    {
        attempts.Clear();
    }

    #region Gates access
    public TGate GetGate<TGate>() where TGate : IGate, new()
    {
        var t = typeof(TGate);

        return (TGate)GetGate(t);
    }
    public IGate GetGate(Type gateType)
    {
        if (gateType == null) throw new ArgumentNullException("gateType");
        if (!igateType.IsAssignableFrom(gateType)) throw new Exception("Provided gateType is not of IGate");

        if (!gates.ContainsKey(gateType) || gates[gateType] == null)
            gates[gateType] = (IGate)Activator.CreateInstance(gateType);

        return gates[gateType];
    }
    /// <summary>
    /// Set a specific instance of a gate for a type
    /// </summary>
    /// <typeparam name="TGate"></typeparam>
    /// <param name="gate">can be null to reset the gate for that TGate</param>
    public void SetGate<TGate>(TGate gate) where TGate : IGate
    {
        var t = typeof(TGate);
        SetGate(t, gate);
    }
    /// <summary>
    /// Set a specific instance of a gate for a type
    /// </summary>
    /// <param name="gateType"></param>
    /// <param name="gate">can be null to reset the gate for that gateType</param>
    public void SetGate(Type gateType, IGate gate)
    {
        if (gateType == null) throw new ArgumentNullException("gateType");
        if (!igateType.IsAssignableFrom(gateType)) throw new Exception("Provided gateType is not of IGate");

        gates[gateType] = gate;
    }
    #endregion

}

测试

我已经为此编写了一个测试夹具:

[TestFixture]
public class SecurityDelayManagerTest
{
    static MyTestLoginGate gate;
    static SecurityDelayManager manager;

    [SetUp]
    public void TestSetUp()
    {
        manager = SecurityDelayManager.Instance;
        gate = new MyTestLoginGate();
        manager.SetGate(gate);
    }

    [TearDown]
    public void TestTearDown()
    {
        manager.ResetAttempts();
    }

    [Test]
    public void Test_SingleFailedAttemptCheck()
    {
        var attempt = gate.CreateLoginAttempt(false, "user1");
        Assert.IsNotNull(attempt);

        manager.Check(gate, attempt);
        Assert.AreEqual(1, gate.AttemptsCount);
    }

    [Test]
    public void Test_AttemptExpiration()
    {
        var attempt = gate.CreateLoginAttempt(false, "user1");
        Assert.IsNotNull(attempt);

        manager.Check(gate, attempt);
        Assert.AreEqual(1, gate.AttemptsCount);
    }

    [Test]
    public void Test_SingleSuccessfulAttemptCheck()
    {
        var attempt = gate.CreateLoginAttempt(true, "user1");
        Assert.IsNotNull(attempt);

        manager.Check(gate, attempt);
        Assert.AreEqual(0, gate.AttemptsCount);
    }

    [Test]
    public void Test_ManyAttemptChecks()
    {
        for (int i = 0; i < 20; i++)
        {
            var attemptGood = gate.CreateLoginAttempt(true, "user1");
            manager.Check(gate, attemptGood);

            var attemptBaad = gate.CreateLoginAttempt(false, "user1");
            manager.Check(gate, attemptBaad);
        }

        Assert.AreEqual(20, gate.AttemptsCount);
    }

    [Test]
    public void Test_GateAccess()
    {
        Assert.AreEqual(gate, manager.GetGate<MyTestLoginGate>(), "GetGate should keep the same gate");
        Assert.AreEqual(gate, manager.GetGate(typeof(MyTestLoginGate)), "GetGate should keep the same gate");

        manager.SetGate<MyTestLoginGate>(null);

        var oldGate = gate;
        var newGate = manager.GetGate<MyTestLoginGate>();
        gate = newGate;

        Assert.AreNotEqual(oldGate, newGate, "After a reset, new gate should be created");

        manager.ResetAttempts();
        Test_ManyAttemptChecks();

        manager.SetGate(typeof(MyTestLoginGate), oldGate);

        manager.ResetAttempts();
        Test_ManyAttemptChecks();
    }
}


public class MyTestLoginGate : SecurityDelayManager.IGate
{
    #region Static
    static Guid myID = new Guid("81e19a1d-a8ec-4476-a187-5130361a9006");
    static TimeSpan myTF = TimeSpan.FromHours(24);

    class LoginAttempt : Attempt { }
    class PasswordResetAttempt : Attempt { }
    abstract class Attempt : SecurityDelayManager.IAttempt
    {
        public bool Successful { get; set; }
        public DateTime Time { get; set; }
        public String UserName { get; set; }

        public string SerializeForAuditLog()
        {
            return ToString();
        }
        public override string ToString()
        {
            return String.Format("Attempt {2} Successful:{0} @{1}", Successful, Time, GetType().Name);
        }
    }
    #endregion

    #region Test properties
    public int AttemptsCount { get; private set; }
    #endregion

    #region Implementation of SecurityDelayManager.IGate
    public Guid AccountID { get { return myID; } }
    public bool ConsiderSuccessfulAttemptsToo { get { return false; } }
    public TimeSpan SecurityTimeFrame { get { return myTF; } }

    public SecurityDelayManager.IAttempt CreateLoginAttempt(bool success, string userName)
    {
        return new LoginAttempt() { Successful = success, UserName = userName, Time = DateTime.Now };
    }
    public SecurityDelayManager.IAttempt CreatePasswordResetAttempt(bool success, string userName)
    {
        return new PasswordResetAttempt() { Successful = success, UserName = userName, Time = DateTime.Now };
    }

    public SecurityDelayManager.ActionResult Action(SecurityDelayManager.IAttempt attempt, int attemptsCount)
    {
        AttemptsCount = attemptsCount;

        return attemptsCount < 3
            ? SecurityDelayManager.ActionResult.NotDelayed
            : attemptsCount < 30
            ? SecurityDelayManager.ActionResult.Delayed
            : SecurityDelayManager.ActionResult.Emergency;
    }
    #endregion
}

另一个要求:无需 JavaScript :) 是的,即使是现在。 - Robert Cutajar
1
new Guid("81e19a1d-a8ec-4476-a187-5130361a9006"); 在所有客户端之间共享!这里的问题是,如果不同的客户端尝试使用错误密码(> 3次)登录,所有客户端都会面临延迟! - Kashif Faraz
嘿@KashifFaraz,我很高兴这篇10年前的文章对你仍然有价值。感谢您的代码审查 :) 现在我可能会寻找一些现成的解决方案。 - Robert Cutajar

2
我会把延迟放在服务器验证部分,这样它就不会尝试验证(自动返回为false,并显示一条消息,告诉用户需要等待多少秒才能再次尝试)。使用thread.sleep将防止一个浏览器再次尝试登录,但它无法阻止分布式攻击,其中某人有多个程序同时尝试以用户身份登录。
另一个可能性是,尝试之间的时间间隔因登录尝试次数而异。所以第二次尝试他们等待一秒钟,第三次可能是2秒钟,第三次是4秒钟,依此类推。这样,您就不必让合法用户在第一次错误输入密码后等待15秒钟才能再次尝试登录。

非常好的观点!虽然我会对用户因为“我们很忙,请稍后再试”而不得不重新输入密码持谨慎态度。在用户至关重要的环境中,我们努力尽可能地减少他们的烦恼。我认为最重要的功能是适当的日志记录、监控、通知、调查和在所有这些之后——教育预防... - Robert Cutajar

2

Kevin提出了一个很好的观点,不想绑定您的请求线程。 一种解决方法是将登录设置为异步请求。 异步过程只需等待您选择的时间(500ms?)。 然后,您就不会阻塞请求线程。


确实是一个非常好的观点。这可以是Ajax技术或页面重定向/刷新。在我的情况下,它必须在没有JavaScript的情况下工作,所以更麻烦...用户也需要能够单击链接。处理等待的票据可能会成为另一个安全漏洞,因此我们需要极度谨慎。也许可以针对IP进行锁定,并确保其很快过期。 - Robert Cutajar

0

我认为这不会帮助你防范DOS攻击。如果你睡眠请求线程,仍允许请求占用你的线程池,并允许攻击者使你的Web服务瘫痪。

你最好的选择可能是基于尝试登录名、源IP等,在多次失败尝试后锁定请求,以试图针对攻击源,而不会损害你的有效用户。


1
关于服务器耗尽的观点是正确的。虽然在第一种情况下选择IP和登录名会使分布式攻击变得复杂,在第二种情况下登录列表攻击也会受到限制。这对于小型网站可能不是问题,但我认为大型解决方案会因此而受到影响。 - Robert Cutajar
如果处理程序是异步的,那么Task.Delay()是否会释放线程池线程以服务其他请求?我想连接和请求状态仍然占用内存。 - Co-der

0

我知道这不是你要求的,但你可以实现一个账户锁定功能。这样,你可以让他们猜测一定次数后,再让他们等待任意长的时间才能继续猜测。:)


2
这将允许攻击者锁定有效用户,有效地发动拒绝服务攻击。 - TGnat
攻击者如何获得您所有的账户登录信息?您应该先堵上这个漏洞。 - JP Alioto
在TGnat上点头。在一些系统中,比如我的系统,用户名并不是一个秘密,为什么它们应该是呢?我已经从依赖安全性通过模糊性长大了。 - Robert Cutajar

0

我认为你所要求的并不是在Web环境下最有效的方式。登录界面的目的是为“用户”提供一种轻松的方式来访问您的服务,并且应该易于使用和快速。因此,您不应该让用户等待,因为99%的用户不会有恶意。

Sleep.Trhead也有可能在有大量并发用户尝试登录时对您的服务器造成巨大负载。潜在的选择可能包括:

  • 针对x次登录失败的会话结束时(例如)阻止IP
  • 提供验证码

当然,这些不是所有选项,但我相信更多人会有更多想法...


嘿,我同意大部分情况下事情应该快速进行,但是像参考文章中所解释的那样,真的有必要想出某种限制措施。Thread.Sleep确实会让你从一个麻烦陷阱跌入另一个。然而,封锁IP对于身处大墙后面的用户来说高度歧视,并且无法解决分布式攻击问题。对于验证码-用户花费时间解决那个恼人谜题的时间可以轻松愉快地等待而不会有任何麻烦。 - Robert Cutajar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接