3

好的 ASP.NET WebForms 现在是旧技术,但在可预见的未来我们会坚持使用它,我只是想看看我是否可以放入一个非常基本的重放攻击预防机制。基本上,我们希望确保在用户明确注销后,没有人可以重播旧请求并根据授权 cookie 自动对其进行身份验证,但我们仍然希望允许对不想要的用户使用永久授权 cookie重新登录每个新的浏览器会话。根据我在后一种情况下观察到的情况,如果您退出浏览器并开始新会话,则没有cookie 标头中的会话 ID - 仅表单授权 cookie。在这种情况下,我们希望允许自动身份验证(至少对于 GET 请求)。在“重播”的情况下,cookie 标头中有一个会话 ID - 但 ASP.NET 实际上会使用相同的 ID 自动重新创建一个新会话。幸运的是,我们可以检测到这一点(通过检查 Session.IsNewSession 和 Request.Headers["Cookie"] - 虽然不是,奇怪的是,Request.Cookies 有时包含会话 cookie,即使它不是由客户端发送的),因此它是如果客户端已为用户注销时已关闭/放弃的会话发送会话 ID,则可以强制重新登录。

但是……如果重放攻击故意忽略会话 ID,那么当授权 cookie 是永久 cookie 时,就没有真正的方法可以将其与合法的浏览器请求区分开来。在这种情况下,您至少可以阻止“POST”请求,但是重放攻击可以先发出 GET 来建立新会话,然后再使用 POST。我真的什么want 是一种确定用户注销后用于授权该会话的 cookie 值不再有效的方法 - 我想这需要在数据库中存储一些东西(很可能是当前有效 cookie 值的列表) ,这似乎比目前所能证明的要付出更多的努力。但似乎其他任何事情(例如在表单上使用隐藏的 nonce 字段)都容易出现一个脚本,该脚本只是模拟用户使用永久授权 cookie 重新建立新会话,在这种情况下,他们将能够确定所需的nonce 字段值等因此假设我是正确的,必须将某些内容存储在服务器端以跟踪(基本上)哪些授权 cookie 值仍然有效,是否有任何已知的低占用空间/知名库做这个?

(顺便说一句,现在我在身份验证票证上使用了 IsPersistent 标志 - 如果这是错误的,即用户已明确选择仅使用每个会话 cookie,因为他们知道他们将重新登录下一个浏览器会话,那么我可以可靠地阻止重放攻击。但如果是真的,我只阻止“POST”重放,这并没有提供太多真正的保护,但至少担心更复杂的重放攻击的用户可以通过始终选择每个会话来阻止它们验证)。

4

2 回答 2

4

甚至 ASP.NET 身份验证都明确表示您必须进行二次检查以确认用户是否仍然是活动的登录用户(例如,我们可以阻止用户,用户可能已经更改了他的密码),表单身份验证票不提供任何针对这些事情的安全措施。

UserSession 与 ASP.NET MVC Session 无关,这里只是一个名字

我实施的解决方案是,

  1. 在数据库中创建一个UserSessionsUserSessionID (PK, Identity) UserID (FK) DateCreated, DateUpdated
  2. FormsAuthenticationTicket 有一个名为 UserData 的字段,您可以在其中保存 UserSessionID。

用户登录时

public void DoLogin(){
     
     // do not call this ...
     // FormsAuthentication.SetAuthCookie(....

     DateTime dateIssued = DateTime.UtcNow;

     var sessionID = db.CreateSession(UserID);
     var ticket = new FormsAuthenticationTicket(
            userName,
            dateIssued,
            dateIssued.Add(FormsAuthentication.Timeout),
            iSpersistent,
            // userData
            sessionID.ToString());

     HttpCookie cookie = new HttpCookie(
         FormsAuthentication.CookieName,
         FormsAuthentication.Encrypt(ticket));
     cookie.Expires = ticket.Expires;
     if(FormsAuthentication.CookieDomain!=null)
         cookie.Domain = FormsAuthentication.CookieDomain;
     cookie.Path = FormsAuthentication.CookiePath;
     Response.Cookies.Add(cookie);
            
}

授权用户

Global.asax 类可以连接到 Authorize

public void Application_Authorize(object sender, EventArgs e){
     var user = Context.User;
     if(user == null)   
         return;

     FormsIdentity formsIdentity = user.Identity as FormsIdentity;
     long userSessionID = long.Parse(formsIdentity.UserData);
     
     string cacheKey = "US-" + userSessionID;

     // caching to improve performance
     object result = HttpRuntime.Cache[cacheKey];
     if(result!=null){
         // if we had cached that user is alright, we return..
         return;
     }

     // hit the database and check if session is alright
     // If user has logged out, then all UserSessions should have been
     // deleted for this user
     UserSession session = db.UserSessions
           .FirstOrDefault(x=>x.UserSessionID == userSessionID);
     if(session != null){

          // update session and mark last date
          // this helps you in tracking and you
          // can also delete sessions which were not
          // updated since long time...
          session.DateUpdated = DateTime.UtcNow;
          db.SaveChanges();

          // ok user is good to login
          HttpRuntime.Cache.Add(cacheKey, "OK", 
               // set expiration for 5 mins
               DateTime.UtcNow.AddMinutes(5)..)

         // I am setting cache for 5 mins to avoid
         // hitting database for all session validation
         return;
     }

     // ok validation is wrong....


     throw new UnauthorizedException("Access denied");
 
}

用户注销时

public void Logout(){

    // get the ticket..
    FormsIdentity f = Context.User.Identity as FormsIdentity;
    long sessionID = long.Parse(f.UserData);

    var session = db.UserSessions.First(x=>x.UserSessionID = sessionID);
    db.UserSession.Remove(session);
    db.SaveChanges();

    FormsAuthentication.Signout();
}

** 当用户更改密码或用户被阻止或用户被删除时...... **

public void ChangePassword(){

    // get the ticket..
    FormsIdentity f = Context.User.Identity as FormsIdentity;
    long sessionID = long.Parse(f.UserData);

    var session = db.UserSessions.First(x=>x.UserSessionID = sessionID);

    // delete all sessions for the same user id
    // this will force user to relogin on all other
    // devices...
    db.Database.ExecuteSql(
        "DELETE FROM UserSessions WHERE UserID=@UserID",
        new SqlParameter("@UserID", session.UserID));
}
于 2015-09-11T06:16:20.780 回答
0

根据维基百科,会话重放攻击是在您重复相同的请求数据时。

一个非常简单的解决方案是使用一次性密码”(OTP)。也就是说,将您的会话链接到一个 OTP。一旦收到请求,就使 OTP 无效。

于 2020-10-29T08:18:14.363 回答