Как отложить / ограничить попытки входа в систему в ASP.NET? - PullRequest
9 голосов
/ 17 апреля 2009

Я пытаюсь сделать очень простое регулирование запросов в моем веб-проекте ASP.NET. В настоящее время я не заинтересован в глобальном регулировании запросов на DOS-атаки, но хотел бы искусственно отложить ответ на все попытки входа в систему, просто чтобы немного усложнить атаки по словарю (более или менее, как Джефф Этвуд, обрисованный в общих чертах здесь ).

Как бы вы это реализовали? Наивный способ сделать это - я полагаю - просто позвонить

Thread.Sleep();

где-то во время запроса. Предложения? :)

Ответы [ 6 ]

4 голосов
/ 08 декабря 2011

У меня была такая же идея, как и у вас, как улучшить безопасность экрана входа в систему (и экранов сброса пароля). Я собираюсь реализовать это для своего проекта и поделюсь с вами своей историей.

Требования

Мои требования заключаются в следующих пунктах:

  • Не блокируйте отдельных пользователей только потому, что кто-то пытается взломать
  • Мои имена пользователей очень легко угадать, потому что они следуют определенному шаблону (а мне не нравится безопасность по неизвестности)
  • Не тратьте ресурсы сервера, спя на слишком много запросов, очередь в конечном итоге переполнится, и запросы начнут истекать время ожидания
  • Предоставлять быстрый сервис большинству пользователей в 99% случаев
  • Устранить атаки методом перебора на экране входа в систему
  • Также обрабатывает распределенные атаки
  • Должен быть достаточно потокобезопасным

План

Итак, у нас будет список неудачных попыток и их отметка времени. Каждый раз, когда у нас будет попытка входа в систему, мы будем проверять этот список, и чем больше будет неудачных попыток, тем больше времени потребуется для входа в систему. Каждый раз мы удаляем старые записи по их отметке времени. При превышении определенного порога вход в систему не будет разрешен, и все запросы входа в систему будут немедленно отклонены (аварийное завершение атаки).

Мы не останавливаемся с автоматической защитой. В случае аварийного отключения администратору следует направить уведомление, чтобы можно было расследовать инцидент и принять меры по возмещению ущерба. Наши журналы должны содержать надежную запись неудачных попыток, включая время, имя пользователя и IP-адрес источника для расследования.

План состоит в том, чтобы реализовать это как статически объявленную очередь, где неудачные попытки ставят в очередь и старые записи снимают очередь. длина очереди - наш индикатор серьезности. Когда код будет готов, я обновлю ответ. Я мог бы также включить предложение Keltex - быстро опубликовать ответ и завершить регистрацию с другим запросом.

Обновление: отсутствуют две вещи:

  1. Перенаправление ответа на страницу ожидания, чтобы не засорять очередь запросов, и это, очевидно, немного важная вещь. Нам нужно дать пользователю токен, чтобы позже проверить его с другим запросом. Это может быть еще одна дыра в безопасности, поэтому мы должны быть крайне осторожны с этим. Или просто удалите этот Thread.Sleap (xxx) в методе Action :)
  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 реализован внутри AppCode проекта ASP.NET, поэтому он имеет доступ ко всем интерфейсным вкусностям. Он реализует интерфейс 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

}

Бэкэнд, несколько поточно-ориентированное управление

Итак, этот класс (в моей базовой lib) будет обрабатывать многопоточный подсчет попыток:

/// <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
}
2 голосов
/ 17 апреля 2009

Кевин делает хороший вывод о том, что не хочет связывать вашу ветку запросов. Одним из ответов было бы сделать логин асинхронным запросом . Асинхронный процесс будет просто ждать, сколько времени вы выберете (500 мс?). Тогда вы не заблокируете поток запроса.

1 голос
/ 17 апреля 2009

Я бы поместил задержку в часть проверки сервера, где он не будет пытаться выполнить проверку (автоматически возвращается значение false, если появляется сообщение о том, что пользователь должен ждать столько секунд, прежде чем предпринять другую попытку). другой ответ, пока не прошло столько секунд. Выполнение thread.sleep не позволит одному браузеру предпринять другую попытку, но не остановит распределенную атаку, когда у кого-то есть несколько программ, пытающихся войти в систему как пользователь одновременно.

Другая возможность заключается в том, что время между попытками зависит от количества попыток входа в систему. Итак, вторая попытка у них - одна секунда ожидания, третья - возможно, 2, третья - 4 и так далее. Таким образом, у вас не будет законного пользователя, который должен ждать 15 секунд между попытками входа в систему, потому что он неправильно набрал свой пароль в первый раз.

0 голосов
/ 17 апреля 2009

Я не думаю, что то, о чем вы просите, является достаточно эффективным способом в веб-среде. Цель экранов входа в систему заключается в том, чтобы предоставить «пользователям» легкий доступ к вашим услугам, и они должны быть простыми и быстрыми в использовании. Таким образом, вы не должны заставлять пользователя ждать, так как 99% из них не будут злонамеренными.

Sleep.Trhead также потенциально может создать огромную нагрузку на ваш сервер, если будет много одновременных пользователей, пытающихся войти в систему. Возможные варианты:

  • Блокировка IP-адреса (например, для окончания сеанса) для x количества неудачных попыток входа в систему
  • Введите код с картинки

конечно, это не все варианты, но все же я уверен, что у большего числа людей будет больше идей ...

0 голосов
/ 17 апреля 2009

Я знаю, что это не то, что вы спрашиваете, но вы могли бы вместо этого реализовать блокировку учетной записи. Таким образом, вы даете им свои догадки, а затем можете заставить их ждать сколько угодно времени, прежде чем они снова начнут гадать. :)

0 голосов
/ 17 апреля 2009

Не думаю, что это поможет вам предотвратить атаки DOS. Если вы спите в потоке запросов, вы по-прежнему разрешаете запросу занимать пул потоков и по-прежнему разрешаете злоумышленнику поставить ваш веб-сервис на колени.

Лучшим вариантом может быть блокировка запросов после указанного числа неудачных попыток на основании предпринятого логина, IP-адреса источника и т. Д., Чтобы попытаться нацелить источник атаки без ущерба для ваших действительных пользователей.

...