我正在尝试在我的ASP.NET Web项目上进行一些非常简单的请求限制。目前我对于全局限制针对DOS攻击并不感兴趣,但是希望人为地延迟所有登录尝试的响应,以使字典攻击变得更加困难(就像Jeff Atwood在这里中概述的那样)。
你会如何实现它呢?做法似乎很简单-只需调用
Thread.Sleep();
请求过程中的某个位置。有建议吗?:)
我正在尝试在我的ASP.NET Web项目上进行一些非常简单的请求限制。目前我对于全局限制针对DOS攻击并不感兴趣,但是希望人为地延迟所有登录尝试的响应,以使字典攻击变得更加困难(就像Jeff Atwood在这里中概述的那样)。
你会如何实现它呢?做法似乎很简单-只需调用
Thread.Sleep();
请求过程中的某个位置。有建议吗?:)
我和你一样,想要提高登录页面(和密码重置页面)的安全性。我将在我的项目中实现这个功能,并与你分享我的经历。
我的需求如下:
所以我们将有一个失败尝试列表和其时间戳。每次进行登录尝试时,我们都会检查这个列表,如果有更多的失败尝试,登录时间就会更长。每次我们都会根据时间戳删除旧条目。超过某个阈值后,将不允许登录,并立即失败所有登录请求(攻击紧急关闭)。
我们不会止步于自动保护。在紧急关闭情况下,应向管理员发送通知,以便进行调查并采取修复措施。我们的日志应包含失败尝试的坚实记录,包括时间、用户名和源 IP 地址,以供调查。
计划将此实现为静态声明队列,其中失败尝试进入队列,旧条目出队列。队列长度是我们的严重程度指标。当我准备好代码后,我会更新答案。我可能还会包括 Keltex 的建议 - 快速释放响应并使用另一个请求完成登录。
更新:有两个缺失:
让我们看看最终能否解决这些问题...
ASP.NET UI 页面应该尽量简化,然后我们可以像这样获取 Gate 的实例:
static private LoginGate Gate = SecurityDelayManager.Instance.GetGate<LoginGate>();
登录(或密码重置)尝试后,请调用以下函数:
SecurityDelayManager.Instance.Check(Gate, Gate.CreateLoginAttempt(success, UserName));
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
}
Kevin提出了一个很好的观点,不想绑定您的请求线程。 一种解决方法是将登录设置为异步请求。 异步过程只需等待您选择的时间(500ms?)。 然后,您就不会阻塞请求线程。
我认为这不会帮助你防范DOS攻击。如果你睡眠请求线程,仍允许请求占用你的线程池,并允许攻击者使你的Web服务瘫痪。
你最好的选择可能是基于尝试登录名、源IP等,在多次失败尝试后锁定请求,以试图针对攻击源,而不会损害你的有效用户。
我知道这不是你要求的,但你可以实现一个账户锁定功能。这样,你可以让他们猜测一定次数后,再让他们等待任意长的时间才能继续猜测。:)
我认为你所要求的并不是在Web环境下最有效的方式。登录界面的目的是为“用户”提供一种轻松的方式来访问您的服务,并且应该易于使用和快速。因此,您不应该让用户等待,因为99%的用户不会有恶意。
Sleep.Trhead也有可能在有大量并发用户尝试登录时对您的服务器造成巨大负载。潜在的选择可能包括:
当然,这些不是所有选项,但我相信更多人会有更多想法...