9 Stimmen

Wie verzögert / drosselt man Anmeldeversuche in ASP.NET?

Ich versuche, einige sehr einfache Anfrage Drosselung auf meine ASP.NET-Web-Projekt zu tun. Momentan bin ich nicht daran interessiert, Anfragen global gegen DOS-Angriffe zu drosseln, sondern möchte die Antwort auf alle Anmeldeversuche künstlich verzögern, nur um Wörterbuchangriffe ein bisschen schwieriger zu machen (mehr oder weniger wie Jeff Atwood skizziert aquí ).

Wie würden Sie es umsetzen? Der näiveaulose Weg wäre - so vermute ich -, einfach aufzurufen

Thread.Sleep();

irgendwo während der Anfrage. Vorschläge? :)

4voto

Robert Cutajar Punkte 2444

Ich habe die gleiche Idee wie Sie, wie man die Sicherheit eines Anmeldebildschirms (und eines Bildschirms zum Zurücksetzen des Passworts) verbessern kann. Ich werde dies für mein Projekt implementieren und meine Geschichte mit Ihnen teilen.

Anforderungen

Meine Anforderungen sind die folgenden Punkte:

  • Sperren Sie nicht einzelne Benutzer, nur weil jemand versucht, sich einzuhacken
  • Meine Benutzernamen sind sehr leicht zu erraten, weil sie einem bestimmten Muster folgen (und ich mag keine Sicherheit durch Unklarheit)
  • Vergeuden Sie keine Server-Ressourcen, indem Sie auf zu vielen Anfragen schlafen, da die Warteschlange sonst überläuft und die Anfragen nicht mehr bearbeitet werden können.
  • den meisten Nutzern in 99 % der Fälle einen schnellen Service zu bieten
  • Beseitigung von Brute-Force-Angriffen auf den Anmeldebildschirm
  • Auch verteilte Angriffe bewältigen
  • Muss einigermaßen thread-sicher sein

Plan

Wir werden also eine Liste der fehlgeschlagenen Versuche und deren Zeitstempel haben. Bei jedem Anmeldeversuch überprüfen wir diese Liste, und je mehr Fehlversuche es gibt, desto länger dauert es, sich anzumelden. Jedes Mal werden wir alte Einträge anhand ihres Zeitstempels löschen. Ab einem bestimmten Schwellenwert werden keine Anmeldungen mehr zugelassen und alle Anmeldeversuche werden sofort abgebrochen (Angriffs-Notabschaltung).

Mit dem automatischen Schutz ist es nicht getan. Im Falle einer Notabschaltung sollte eine Benachrichtigung an die Administratoren geschickt werden, damit der Vorfall untersucht und Wiederherstellungsmaßnahmen ergriffen werden können. In unseren Protokollen sollten die fehlgeschlagenen Versuche einschließlich der Uhrzeit, des Benutzernamens und der Quell-IP-Adresse für die Untersuchung festgehalten werden.

Der Plan ist, dies als statisch deklarierte Warteschlange zu implementieren, bei der sich fehlgeschlagene Versuche in die Warteschlange einreihen und alte Einträge aus der Warteschlange entfernt werden. die Länge der Warteschlange ist unser Indikator für den Schweregrad. Wenn ich den Code fertig habe, werde ich die Antwort aktualisieren. Ich könnte auch den Vorschlag von Keltex einbeziehen - die Antwort schnell freizugeben und die Anmeldung mit einer weiteren Anfrage abzuschließen.

Update: Es fehlen noch zwei Dinge:

  1. Die Umleitung der Antwort auf eine Warteseite, um die Anfragewarteschlange nicht zu verstopfen und das ist natürlich ein kleiner Knackpunkt. Wir müssen dem Benutzer ein Token geben, damit er sich später mit einer anderen Anfrage zurückmeldet. Dies könnte eine weitere Sicherheitslücke sein, so dass wir hier extrem vorsichtig sein müssen. Oder lassen Sie den Thread.Sleap(xxx) in der Action-Methode einfach weg :)
  2. Die IP, dooh, nächstes Mal...

Mal sehen, ob wir da irgendwann durchkommen...

Was getan wurde

ASP.NET-Seite

ASP.NET UI Page sollte ein Minimum an Aufwand haben, dann bekommen wir eine Instanz eines Gates wie dieses:

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

Und nach dem Versuch, sich anzumelden (oder das Passwort zurückzusetzen), rufen Sie an:

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

ASP.NET-Verarbeitungscode

Das LoginGate ist im AppCode des ASP.NET-Projekts implementiert, so dass es Zugriff auf alle Front-End-Funktionen hat. Es implementiert die Schnittstelle IGate, die von der Backend-Instanz SecurityDelayManager verwendet wird. Die Action-Methode muss mit einer Warteumleitung abgeschlossen werden.

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

}

Backend einigermaßen thread-sicheres Management

Diese Klasse (in meiner Core-Lib) wird also die Multithreading-Zählung der Versuche übernehmen:

/// <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

}

Tests

Und ich habe eine Testvorrichtung dafür gebaut:

[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
}

2voto

kemiller2002 Punkte 110605

Ich würde die Verzögerung auf dem Server Validierung Teil, wo es nicht versuchen, zu validieren (automatisch zurückkommen, wie falsch haben eine Nachricht sagen, der Benutzer hat so viele Sekunden warten, bevor Sie einen weiteren Versuch). eine andere Antwort, bis so viele Sekunden vergangen sind. Mit thread.sleep wird verhindert, dass ein Browser einen weiteren Versuch unternimmt, aber ein verteilter Angriff, bei dem mehrere Programme gleichzeitig versuchen, sich als Benutzer anzumelden, wird damit nicht verhindert.

Eine andere Möglichkeit ist, dass die Zeit zwischen den Versuchen davon abhängt, wie viele Anmeldeversuche unternommen werden. Beim zweiten Versuch wird also eine Sekunde gewartet, beim dritten vielleicht 2, beim dritten 4 und so weiter. Auf diese Weise wird verhindert, dass ein rechtmäßiger Benutzer 15 Sekunden zwischen den Anmeldeversuchen warten muss, weil er sein Passwort beim ersten Mal falsch eingegeben hat.

2voto

Keltex Punkte 25852

Kevin hat ein gutes Argument, wenn es darum geht, den Faden für Ihre Anfrage nicht zu überspannen. Eine Antwort wäre, die Anmeldung zu einem asynchrone Anfrage . Der asynchrone Prozess würde einfach die von Ihnen gewählte Zeitspanne abwarten (500ms?). Dann würden Sie den Anfrage-Thread nicht blockieren.

0voto

mtazva Punkte 1007

Ich glaube nicht, dass Sie damit DOS-Angriffe vereiteln können. Wenn Sie den Anforderungsthread in den Ruhezustand versetzen, können Sie immer noch zulassen, dass die Anforderung Ihren Thread-Pool belegt und dem Angreifer ermöglichen, Ihren Webdienst in die Knie zu zwingen.

Am besten ist es, Anfragen nach einer bestimmten Anzahl fehlgeschlagener Versuche auf der Grundlage des versuchten Anmeldenamens, der Quell-IP-Adresse usw. zu sperren, um zu versuchen, die Quelle des Angriffs zu finden, ohne dass Ihre gültigen Benutzer darunter leiden.

0voto

JP Alioto Punkte 44283

Ich weiß, dass das nicht Ihr Anliegen ist, aber Sie könnten stattdessen eine Kontosperre einrichten. Auf diese Weise geben Sie ihnen ihre Vermutungen und können sie dann beliebig lange warten lassen, bevor sie erneut raten können :)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X