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:
- 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 :)
- 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
}