9

我正在尝试对我的 ASP.NET Web 项目进行一些非常简单的请求限制。目前我对针对 DOS 攻击的全局限制请求不感兴趣,但想人为地延迟对所有登录尝试的响应,只是为了让字典攻击更难做(或多或少像 Jeff Atwood在这里概述的那样)。

你将如何实施它?这样做的幼稚方式是 - 我想 - 简单地调用

Thread.Sleep();

在请求期间的某个地方。建议?:)

4

6 回答 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 中实现,因此它可以访问所有前端好东西。它实现了后端 SecurityDelayManager 实例使用的接口 IGate。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
}
于 2011-12-08T13:49:56.230 回答
2

我会将延迟放在服务器验证部分,它不会尝试验证(自动返回,因为 false 有一条消息说用户必须等待这么多秒才能再次尝试)。另一个答案,直到过了这么多秒。执行 thread.sleep 将阻止一个浏览器进行另一次尝试,但它不会阻止分布式攻击,即有人有多个程序试图同时以用户身份登录。

另一种可能性是尝试之间的时间因尝试登录的次数而异。所以第二次尝试他们等待一秒钟,第三次可能是 2,第三次是 4,依此类推。这样,您就不必让合法用户在两次登录尝试之间等待 15 秒,因为他们第一次输入错误的密码。

于 2009-04-17T17:14:57.540 回答
2

Kevin 提出了一个很好的观点,即不想占用您的请求线程。一个答案是让登录成为一个异步请求。异步过程只是等待您选择的时间量(500ms?)。那么你就不会阻塞请求线程。

于 2009-04-17T18:10:45.923 回答
0

我认为您所要求的在网络环境中并不是一种非常有效的方式。登录屏幕的目的是为“用户”提供访问您的服务的简便方法,并且应该易于使用且快速。所以你不应该让用户等待,因为他们中的 99% 都不会坏。

如果有很多并发用户尝试登录,Sleep.Trhead 也有可能给您的服务器带来巨大的负载。可能的选项是:

  • 阻止(例如)会话结束的 IP 进行 x 次不成功的登录尝试
  • 提供验证码

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

于 2009-04-17T18:32:57.010 回答
0

我认为这不会帮助您阻止 DOS 攻击。如果您休眠请求线程,您仍然允许请求占用您的线程池,并且仍然允许攻击者使您的 Web 服务屈服。

您最好的选择可能是根据尝试的登录名、源 IP 等在指定次数的失败尝试后锁定请求,以尝试定位攻击源,而不会损害您的有效用户。

于 2009-04-17T17:16:56.850 回答
0

I know it's not what you're asking, but you could implement an account lockout instead. That way, you give them their guesses and then can make them wait any amount of time you want before they can start guessing again. :)

于 2009-04-17T18:13:31.220 回答