65.9K
CodeProject 正在变化。 阅读更多。
Home

10 点保护您的 ASP.NET Core MVC 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (31投票s)

2018 年 9 月 5 日

CPOL

37分钟阅读

viewsIcon

76050

downloadIcon

494

如何保护 ASP.NET Core MVC 应用程序免受十大攻击

引言

我们是 .NET Core 框架的新手,并且正在使用它来开发生产应用程序,但在开发生产应用程序时,我们还必须考虑安全性。因此,在本文中,我们将介绍 10 个有助于我们保护 ASP.NET Core MVC 代码安全的要点。

目录

  1. 身份验证和会话管理失效
  2. 敏感数据泄露与审计日志
  3. 跨站脚本 (XSS) 攻击
  4. 恶意文件上传
  5. 安全配置错误(必须设置自定义错误页面)
  6. 版本泄露
  7. 跨站请求伪造 (CSRF)
  8. XML 外部实体 (XXE)
  9. 不安全的反序列化
  10. SQL 注入攻击
  11. 结论
  12. 历史

1. 身份验证和会话管理失效

在这一部分,如果我们未能妥善管理应用程序的身份验证,攻击者可能会窃取用户凭据,例如会话、Cookie,这可能允许攻击者完全访问整个应用程序,然后他们可能会尝试访问应用程序服务器和数据库服务器,从而导致大规模数据泄露。

攻击者可能窃取数据的方式

  • 不安全连接(未使用的 SSL)
  • 可预测的登录凭据
  • 未以加密形式存储凭据
  • 应用程序登出不当

可能发生的攻击

会话固定

在找到预防此攻击的方法之前,让我们先来看一个会话固定攻击如何发生的简短演示。

每当用户向服务器发送第一个请求时,就会加载登录页面,然后用户输入有效的登录凭据以登录 Web 应用程序。成功登录后,我们在会话中分配一些值以识别唯一用户,同时在浏览器中添加一个 [".AspNetCore.Session"] Cookie,用于识别发送请求的特定用户,并且 [".AspNetCore.Session"] Cookie 值将始终发送到服务器,直到您从应用程序中登出为止。登出时,我们基本上会编写代码来删除创建的会话值,但我们不会删除登录时创建的 [".AspNetCore.Session"] Cookie。此值有助于攻击者执行会话固定攻击。

图 1. 会话固定

用户输入有效凭据后

输入有效登录凭据后,[".AspNetCore.Session"] Cookie 会被添加到浏览器。

注意:当任何数据保存到会话中时,[".AspNetCore.Session"] Cookie 就会被创建并添加到用户浏览器。

图 2. 显示 Cookie 管理器中的应用程序 Cookie

从应用程序登出后 Cookie 仍然存在于浏览器中

从应用程序登出后,[".AspNetCore.Session"] Cookie 仍然存在。

图 3. 登出应用程序后显示 Cookie 管理器中的应用程序 Cookie。

注意:预 Cookie 和后 Cookie 相同,这可能导致会话固定。

让我们进行一些会话固定演示

登出后未删除的 [".AspNetCore.Session"] Cookie 有助于攻击者进行会话固定。我将打开一个浏览器(Chrome),在其中输入我们将要进行会话固定的应用程序的 URL [https://:53654/]。

图 4. 登录页面。

在此视图中,我展示了用户登录时在 Firefox 浏览器中创建的 [".AspNetCore.Session"] Cookie。

图 5. 登录应用程序后显示创建的 Cookie。

Firefox 浏览器中已创建的 Cookie。

注意:为了管理 Cookie,我在 Chrome 浏览器中安装了 Cookie Manager+ 插件。

在浏览器中输入 URL,现在让我们检查一下是否创建了 [".AspNetCore.Session"] Cookie。哦,我们没有任何 Cookie。

图 5. 显示 Cookie。

在查看了 Firefox 中的 [".AspNetCore.Session"] Cookie 后,现在让我们在 Chrome 浏览器中创建与 Firefox 浏览器中的 Cookie 类似的 [".AspNetCore.Session"] Cookie,使用相同的 [".AspNetCore.Session"] Cookie 名称和值。

在 Chrome 浏览器中创建了与 Firefox 浏览器中创建的 Cookie 类似的新的 [".AspNetCore.Session"] Cookie。

这是我们固定会话的步骤。此会话在另一个浏览器 [Firefox] 上有效,我们复制了类似的值并创建了一个 [".AspNetCore.Session"] Cookie,并将 SessionID 的类似值分配给了此 Cookie。

图 6. 复制旧 Cookie 值以创建新的会话 Cookie。

注意:为了添加 Cookie,我在 Chrome 浏览器中安装了 Edit this Cookie 插件。

图 7. 通过复制旧 Cookie 值创建新 Cookie。

固定 Cookie 后,现在我们不再需要登录应用程序,如果我们只需输入应用程序的内部 URL,就可以直接访问,因为此会话是在身份验证后创建的。

我的内部 URL 是:https://:53654/Dashboard/Index。

下面,您可以看到我无需登录应用程序即可访问仪表板页面,这是通过会话固定的。

图 8. 创建新的会话 Cookie 后的视图。

解决方案

  1. 登出后移除 [".AspNetCore.Session"]
  2. 使用 SSL 保护 Cookie 和会话
  3. 通过设置 HTTP Only 来保护 Cookie

登出后移除 [".AspNetCore.Session"]

登出时,我们正在移除 Session 值。同时,我们正在从浏览器中移除 [".AspNetCore.Session"] Cookie。

我们在 ASP.NET MVC 中也完成了类似的过程,但 ASP.NET Core MVC 中的 Cookie 名称不同。

代码片段

[HttpGet]
public ActionResult Logout()
{
    try
    {
        // Removing Session
        HttpContext.Session.Clear();

        // Removing Cookies
        CookieOptions option = new CookieOptions();
        if (Request.Cookies[".AspNetCore.Session"] != null)
        {
            option.Expires = DateTime.Now.AddDays(-1);
            Response.Cookies.Append(".AspNetCore.Session", "", option);
        }

        if (Request.Cookies["AuthenticationToken"] != null)
        {
            option.Expires = DateTime.Now.AddDays(-1);
            Response.Cookies.Append("AuthenticationToken", "", option);
        }
     
        return RedirectToAction("Login", "Account");
    }
    catch (Exception)
    {
        throw;
    }
}

保护 Cookie

为了在登录 [HttpPost] Action Method 时保护 Cookie,我们将创建一个新的 Session,在其中 [Session["AuthenticationToken"]]。我们将保存 NewGuid。同时,我们将添加一个名称为 ["AuthenticationToken"] 的 Cookie,它也将具有与 Session 中存储的相同的 [Guid] 值。

代码片段

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {   //Getting Password from Database
        var storedpassword = ReturnPassword(model.UserName);
        // Comparing Password with Seed
        if (ReturnHash(storedpassword, model.HdrandomSeed) == model.Password)
        {
            HttpContext.Session.SetString("Username", Convert.ToString(model.UserName));

            // Getting New Guid
            string guid = Convert.ToString(Guid.NewGuid());
            //Storing new Guid in Session
            HttpContext.Session.SetString("AuthenticationToken", Convert.ToString(guid));

            //Adding Cookie in Browser
            CookieOptions option = new CookieOptions {Expires = DateTime.Now.AddHours(24)};
            Response.Cookies.Append("AuthenticationToken", guid, option);

            return RedirectToAction("Index", "Dashboard");
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
    return View(model);
}

代码片段说明

  • 创建新的 Guid。
    // Getting New Guid
    string guid = Convert.ToString(Guid.NewGuid());
  • 将新的 Guid 保存到会话中。

    ASP.NET Core 中添加会话的过程与典型的 ASP.NET MVC 应用程序略有不同。

    //Storing new Guid in Session
     HttpContext.Session.SetString("AuthenticationToken", Convert.ToString(guid));
  • 将新的 Guid 保存到 Cookie 中并将 Cookie 插入浏览器。

    ASP.NET Core 中添加 Cookie 的过程与典型的 ASP.NET MVC 应用程序略有不同。

    //Adding Cookie in Browser
     CookieOptions option = new CookieOptions {Expires = DateTime.Now.AddHours(24)};
     Response.Cookies.Append("AuthenticationToken", guid, option);

    在会话中存储数据并在浏览器中添加 Cookie 后,现在我们将匹配每个请求上的这些值并检查这些值是否相同。如果不同,我们将重定向到登录页面。

    为了完成这部分工作,我将向项目中添加一个 AuthorizationFilter,并在其中编写检查 Session 和 Cookie 值是否相同的逻辑。

AuthenticateUser ActionFilter

如果您查看下面的代码片段,我创建了一个名为 AuthenticateUserAuthorizationFilter,该过滤器继承了 IAuthorizationFilter 接口和 FilterAttribute 类。通过这一点,我们在接口 [OnAuthorization] 中实现方法,并在该方法中编写全部逻辑。

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace WebApplication13.Filters
{
    public class AuthenticateUser : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {           
            string tempSession =
                Convert.ToString(context.HttpContext.Session.GetString("AuthenticationToken"));
            string tempAuthCookie =
                Convert.ToString(context.HttpContext.Request.Cookies["AuthenticationToken"]);

            if (tempSession != null && tempAuthCookie != null)
            {
                if (!tempSession.Equals(tempAuthCookie))
                {
                    ViewResult result = new ViewResult {ViewName = "Login"};
                    context.Result = result;
                }
            }
            else
            {
                ViewResult result = new ViewResult {ViewName = "Login"};
                context.Result = result;
            }
        }
    }
}

代码片段说明

在此方法中,我们首先获取会话和 Cookie 的值。

string tempSession =
    Convert.ToString(context.HttpContext.Session.GetString("AuthenticationToken"));
string tempAuthCookie =
    Convert.ToString(context.HttpContext.Request.Cookies["AuthenticationToken"]);

在获取会话和 Cookie 的值后,现在我们将检查会话和 Cookie 的值均不为 null。之后,我们将查看会话和 Cookie 的值是否相等。如果不相等,我们将重定向到登录页面。

  if (tempSession != null && tempAuthCookie != null)
  {
      if (!tempSession.Equals(tempAuthCookie))
      {
          ViewResult result = new ViewResult {ViewName = "Login"};
          context.Result = result;
      }
  }
  else
  {
      ViewResult result = new ViewResult {ViewName = "Login"};
      context.Result = result;
  }

在理解了代码片段后,现在我们将此过滤器应用于用户登录应用程序后访问的每个控制器。

应用 AuthenticateUser 过滤器

将此过滤器应用于用户登录应用程序后访问的每个控制器。

图 9. 应用 AuthenticateUser 过滤器。

在所有用户登录后访问的控制器上应用操作过滤器。

现在,如果攻击者知道 [".AspNetCore.Session"] Cookie 的值和新的 [Cookies["AuthenticationToken"]] Cookie 的值,他们仍然无法进行会话固定攻击,因为新的 [Cookies["AuthenticationToken"]] 包含唯一且值相同的 GUID,并存储在 Web 服务器上的会话 [Session["AuthenticationToken"]] 中,但攻击者无法知道存储在 Web 服务器上的 Session 值,并且这些值在用户每次登录应用程序时都会变化,攻击者用来进行攻击的旧会话值将无法在此场景中使用。

最后,如果我们允许拥有有效 Session["AuthenticationToken"] 值和 Cookies["AuthenticationToken"] 值的用户访问应用程序。

两个 Cookie 的实时值

图 10. 显示两个 Cookie 的值

使用 SSL 保护 Cookie 和会话值

SSL(安全套接字层)是客户端和服务器之间进行安全(加密)通信的层,因此从客户端和服务器传递的任何数据(银行详细信息、密码、会话、Cookie 和其他金融交易)都是安全的(加密的)。

图 11. SSL(安全套接字层)层。

设置 Cookie HttpOnly 为 True

在 ASP.NET Core 中设置 HttpOnly Cookie。

HttpOnly 是在设置 Cookie 时可以用来阻止客户端脚本访问 Cookie 的一个标志。例如,JavaScript 无法读取已设置 HttpOnly 的 Cookie。

代码片段

  1. 创建 Cookie 时设置 HTTP Only
    //Adding Cookie in Browser
    
    string guid = Convert.ToString(Guid.NewGuid());
    CookieOptions option = new CookieOptions {Expires = DateTime.Now.AddHours(24), 
                                              HttpOnly = true};
    Response.Cookies.Append("AuthenticationToken", guid, option);
  2. Startup.cs Configure 方法中全局设置 HTTP Only。

    图 12. 将 Cookie HttpOnly 属性设置为 True。

代码片段

app.UseCookiePolicy(new CookiePolicyOptions
 {
     HttpOnly = HttpOnlyPolicy.Always,
     Secure = CookieSecurePolicy.Always,
     MinimumSameSitePolicy = SameSiteMode.None
 });

2. 敏感数据泄露与审计日志

传输中(发送和接收)的数据以及存储在一个地方的数据应受到保护。由于我们从事 Web 开发,我们存储用户的个人信息(可能包含密码、PAN 号、护照详细信息、信用卡号、健康记录、财务记录、商业秘密)。在这部分,作为开发人员,我看到大多数人只加密密码并将其存储在数据库中,其余数据未加密。如果攻击者获得了此类数据的访问权限,他们就可以滥用这些数据。

解决方案

  • 始终使用带有种子的(随机哈希)强哈希算法将敏感数据以加密格式发送到服务器。
  • 始终为 Web 应用程序应用 SSL。
  • 如果需要存储敏感数据,请使用强哈希技术。

保护登录

在保护之前,让我们看一下大多数开发人员创建的常见登录页面问题。

图 13. 登录页面。

攻击者拦截登录页面以窃取凭据

当用户在登录页面输入凭据并提交到服务器时,数据[用户名和密码]以明文形式传输到服务器,此数据[用户名和密码]可以被攻击者拦截并窃取您的凭据。

图 14. 拦截登录页面,显示明文格式的数据。

始终使用带有种子的强哈希算法将敏感数据以加密格式发送到服务器

在此演示中,我们将使用 MD5 算法和种子在客户端加密数据,然后发送到服务器,以免被攻击者窃取。

  1. 我们将使用 MD5 算法,这是一种哈希算法。
  2. 此过程的第一步是生成一个随机数,然后生成一个作为种子的哈希值,该值将发送到客户端。
  3. 当用户输入用户名和密码并点击登录按钮时,在该时刻,我们生成 MD5 哈希,这是种子+密码的组合。
  4. 哈希后的密码在我们将表单发布到服务器时被接收,然后我们从数据库中按用户名获取哈希后的密码(不含种子),然后我们将密码(不含种子)与种子结合,并将收到的密码与种子进行比较,如果相等,则用户成功登录,否则显示错误消息。

拦截登录页面

在下面的快照中,当用户输入凭据并发布表单时。

图 15. 客户端加密的调试模式。

拦截 POST 请求以显示密码如何以加密形式传输。

图 16. 加密数据后拦截登录页面。

登录视图发布值后的登录操作方法

这里我们可以清楚地看到密码是加密形式的。

在下一步中,我们将比较存储在数据库中的密码。为此,首先,我们将通过用户输入的用户名获取密码,然后将其与数据库中存储的密码进行比较。绿色标记的行是种子值,同时,您可以看到标记为红色的数据库存储密码是我们通过传递用户名获得的,然后黄色行表示此密码是从登录视图发布的,最后蓝色行是数据库密码和种子的组合,我们将其与发布的密码进行比较。

图 17. 在视图中使用 [hdrandomSeed] 属性作为隐藏字段。

使用 SSL 保护 Web 应用程序

SSL(安全套接字层)是用于客户端和服务器之间安全(加密)通信的层,因此[银行详细信息、密码和其他金融交易]等任何数据在客户端和服务器之间传递都是安全的(加密的)。

不要以明文形式在数据库中存储敏感数据

SQL Server 提供加密作为一项新功能来保护数据免受攻击者的攻击。攻击者可能能够访问数据库或表,但由于加密,他们将无法理解数据或使用它。

SQL Server 中加密数据的简单示例

我创建了一个简单的演示表,包含三列。第一列是 int,第二列是 Varchar,第三列是 VARBINARY

创建表后,我们创建了加密的“主密钥”,然后是“证书”和“对称密钥”,然后使用对称密钥加密数据。

下面的快照显示了我们如何加密“Encrycolumn”列数据。

然后使用相同的密钥,我们将解密“Encrycolumn”列数据。

阅读SQL 数据加密和解密的详细过程

以下是可根据需要用于数据加密和解密的算法列表。

哈希算法

如果有人只需要哈希,他们可以使用哈希算法,我们主要使用哈希函数来加密密码。

对称算法

如果有人只需要一个用于加密和解密的密钥,那么他们可以使用对称算法。

非对称算法

如果有人只需要一个用于加密(公钥)和一个用于解密(私钥)的密钥,那么他们可以使用非对称算法。例如,当我们将 Web 服务和 WebAPI 与客户端共享时,可以使用此算法。

哈希算法

  • MD5
  • SHA256
  • SHA384
  • SHA512

对称算法

  • Aes
  • DES
  • RC2
  • Rijndael
  • TripleDES

非对称算法

  • DSA
  • ECDiffieHellman
  • ECDsa
  • RSA

审计日志

在生产应用程序中,会发生数百万笔交易,其中客户端或用户创建数据、更新数据、删除数据,此外,我们还可以深入了解生产应用程序中的任何错误或性能问题,如果有人试图攻击应用程序,我们就可以知道他们的企图。

解决方案

保留 Web 应用程序上所有用户活动的审计日志并始终监控它。

AuditTB 模型

[Table("AuditTB")]
public class AuditTb
{
    [Key]
    public int UsersAuditId { get; set; }
    public int? UserId { get; set; }
    public string SessionId { get; set; }
    public string IpAddress { get; set; }
    public string PageAccessed { get; set; }
    public DateTime? LoggedInAt { get; set; }
    public DateTime? LoggedOutAt { get; set; }
    public string LoginStatus { get; set; }
    public string ControllerName { get; set; }
    public string ActionName { get; set; }
    public string UrlReferrer { get; set; }
    public string Method { get; set; }
}

查看生成的模型后,现在让我们创建一个 AuditTB 表。

AuditTB 表

图 18. AuditTb 表视图。

查看生成的模型后,现在让我们创建一个名为 AuditFilterActionFilter

AuditFilterActionfilter 代码片段

AuditFilter 是我们创建的自定义 ActionFilter。在此过滤器中,我们将用户活动的插入数据放入 AuditTB 表中,同时,我们还检查用户是否已登录应用程序,同样我们也插入访问应用程序的用户的 IP 地址,以及用户登录和登出的时间戳。为了插入这些数据,我们使用了 ORM 实体框架核心。

代码片段

public class AuditFilter : ActionFilterAttribute
{
    private readonly DatabaseContext _databaseContext;
    public AuditFilter(DatabaseContext databaseContext)
    {
        _databaseContext = databaseContext;
    }
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        try
        {
            string actionName = null;
            string controllerName = null;

            // Getting ActionName
            if (((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)
            context.ActionDescriptor).ActionName != null)
            {
                actionName = ((Microsoft.AspNetCore.Mvc.Controllers.
                             ControllerActionDescriptor)context.ActionDescriptor)
                    .ActionName;
            }
            // Getting ControllerName
            if (((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)
            context.ActionDescriptor).ControllerName != null)
            {
                controllerName = ((Microsoft.AspNetCore.Mvc.Controllers.
                ControllerActionDescriptor)context.ActionDescriptor).ControllerName;
            }

            // Assigning values to AuditTb Class
            var objaudit = new AuditTb
            {
                UserId = context.HttpContext.Session.GetInt32("UserID") ?? 0,
                UsersAuditId = 0,
                SessionId = context.HttpContext.Session.Id,
                IpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString(),
                PageAccessed = context.HttpContext.Request.GetDisplayUrl(),
                LoggedInAt = DateTime.Now,
                Method = context.HttpContext.Request.Method
            };

            if (actionName == "Logout")
            {
                objaudit.LoggedOutAt = DateTime.Now;
            }

            objaudit.LoginStatus = "A";
            objaudit.ControllerName = controllerName;
            objaudit.ActionName = actionName;

            _databaseContext.AuditTb.Add(objaudit);
            _databaseContext.SaveChanges();

            base.OnActionExecuting(context);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

全局注册 UserAuditFilter

图 19. 全局注册 UserAuditFilter。

输出

用户请求页面并进行一些活动时插入到审计表中的数据。

图 20. 显示审计表中的数据。

3. 跨站脚本 (XSS) 攻击

跨站脚本 (XSS) 是一种通过输入字段注入恶意脚本的攻击。此攻击非常普遍,允许攻击者窃取凭据和有价值的数据,从而导致严重的安全漏洞。

执行此攻击的方式

  1. 来自输入
  2. 查询字符串
  3. HTTP 头
  4. 来自数据库的数据

为了演示,我创建了一个简单的表单,其中包含两个接受 HTML 作为输入的字段。

图 21. 在输入中输入恶意脚本。

当你看到 JavaScript 被输入到文本字段时,第一个问题是什么?

这应该显示以下错误“检测到来自客户端的潜在危险的 Request.Form 值”。

注意

但在 ASP.NET Core 中,您不会收到此错误,因为 ASP.NET Core 2.0 不拒绝字段中的 HTML。

在发布该表单后,我们没有收到任何错误,并且值已在模型中接收,如下面的快照所示。

图 22. 在输入中输入恶意脚本,这些脚本已填充到模型中。

解决方案

  1. [RegularExpressionAttribute]
  2. 使用 Razor 进行 HTML 编码
  3. URL 编码

XSS 攻击的第一个解决方案是使用正则表达式验证所有字段,这样只有有效数据才能通过。

1. [RegularExpressionAttribute]

使用正则表达式验证输入以防止 XSS 攻击,下方是快照。

应用正则表达式后,我们有了第一道防线,它不允许恶意输入。

图 23. 输入恶意脚本后显示错误消息。

要使用的正则表达式列表

  • 字母和空格 [a-zA-Z ]+$
  • 字母 ^[A-z]+$
  • 数字 ^[0-9]+$
  • 字母数字 ^[a-zA-Z0-9]*$
  • 电子邮件
  • [a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?
  • 手机号码 ^([7-9]{1})([0-9]{9})$
  • 日期格式(mm/dd/yyyy | mm-dd-yyyy | mm.dd.yyyy)
  • /^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\\d\\d+$/
  • 网站 URL ^http(s)?://([\\w-]+.)+[\\w-]+(/[\\w- ./?%&=])?$
  • 信用卡号
  • Visa ^4[0-9]{12}(?:[0-9]{3})?$
  • MasterCard ^5[1-5][0-9]{14}$
  • American Express ^3[47][0-9]{13}$
  • 十进制数((\\d+)((\\.\\d{1,2})?))$

但是正则表达式是针对不允许 HTML 作为输入的字段的,但在我们需要获取 HTML 作为输入的应用程序中,我们需要另一种技术来防御,对吧?

2. 使用 Razor 进行 HTML 编码

MVC 中使用的 Razor 引擎会自动编码所有来自变量的输出,除非您非常努力地阻止它这样做。

对于演示,我已将一些 XSS 攻击脚本存储在数据库中,当我们在视图中渲染它时,这些脚本必须被执行。

图 24. 将恶意脚本存储到数据库中。

当我将这些值显示在 Index 视图上时,它们不会被执行,因为 MVC 中使用的 Razor 引擎会自动编码所有输出值。

在调试模式下显示恶意脚本的渲染方式。

以下是来自 Razor 视图引擎的编码值,它们不允许脚本被执行。

图 25. 显示存储在数据库中的恶意脚本值。

现在我将向您展示,如果您不正确地编码此值会怎样。

在调试模式下显示恶意脚本的渲染方式。

如果我们不正确地编码值,这将允许执行恶意脚本。

图 28. 执行存储在数据库中的恶意脚本。

3. URL 编码

我们主要使用 URL 通过查询字符串将数据从一个开放页面传输到另一个页面,但我们从不编码我们发送的数据,这也会导致 XSS 攻击。

要进行 HTML 编码、URL 编码,我们有内置的库,需要从 NuGet 安装。

图 29. 安装 NuGet 包。

在视图中对 URL 进行编码。

在视图中对 EmailId 字段进行编码后,接下来我们将使用开发人员工具查看编码字段的值。

图 30. 调试模式以显示编码字段的值。

显示如何对 URL 和 HTML 进行编码。

4. 恶意文件上传

我们通常通过应用客户端和服务端验证来保护我们的输入字段,但当涉及到文件上传控件时,我们会忽略验证,我说的对吗?在文件上传中,我们验证的一件事是文件扩展名,如果文件扩展名正确,我们就认为文件是有效的。但在实际场景中,事情并非如此。攻击者可以上传恶意文件,这可能会导致安全问题。攻击者可以更改文件扩展名(tuto.exetuto.jpeg),并且恶意脚本可以作为图像文件上传。大多数开发人员只查看文件的文件扩展名并将其保存在文件夹或数据库中,但文件扩展名是有效的,文件本身可能包含恶意脚本。

解决方案

  • 我们需要做的第一件事是验证文件上传。
  • 只允许访问所需的文件扩展名。
  • 检查文件头。

图 31. 显示带有 HTML 辅助器的文件上传控件。

我们在视图中添加了文件上传控件,接下来将在提交时验证文件。

在 Index HttpPost 方法中验证文件上传

在此方法中,我们首先检查文件上传计数,如果计数为零,则用户未上传任何文件。

如果文件计数大于零,则表示有文件,我们将读取文件的文件名、内容类型和文件字节。

代码片段

[HttpPost]
public IActionResult Document(UploadModel uploadModel)
{
    if (ModelState.IsValid)
    {
        var upload = HttpContext.Request.Form.Files;

        if (HttpContext.Request.Form.Files.Count == 0)
        {
            ModelState.AddModelError("File", "Please Upload Your file");
        }
        else
        {
            foreach (var file in upload)
            {
                if (file.Length > 0)
                {
                    byte[] tempFileBytes = null;
                    var fileName = file.FileName.Trim();

                    using (BinaryReader reader = new BinaryReader(file.OpenReadStream()))
                    {
                        tempFileBytes = reader.ReadBytes((int)file.Length);
                    }

                    var myUniqueFileName = Convert.ToString(Guid.NewGuid());

                    var filetype = Path.GetExtension(fileName).Replace('.', ' ').Trim();

                    var fileExtension = Path.GetExtension(fileName);
                     // Setting Image type
                    var types = CoreSecurity.Filters.FileUploadCheck.FileType.Image;  
                    // Validate Header
                    var result = FileUploadCheck.isValidFile(tempFileBytes, types, filetype); 

                    if (result)
                    {
                        var newFileName = string.Concat(myUniqueFileName, fileExtension);
                        fileName = Path.Combine(_environment.WebRootPath, "images") + 
                                   $@"\{newFileName}";
                        using (FileStream fs = System.IO.File.Create(fileName))
                        {
                            file.CopyTo(fs);
                            fs.Flush();
                        }
                    }
                }
            }
        }
    }
    return View(uploadModel);
}

到目前为止,我们已经完成了基本验证。让我们来验证上传的文件。为此,我编写了一个名为 FileUploadCheck 的静态类。在此类中,有各种用于验证不同文件类型的方法,目前,我将向您展示如何验证图像文件并仅允许图像文件。

图 32. FileUploadCheck 类。

在上面的快照中,有一个 ImageFileExtension enum,其中包含图像格式和文件类型。

private enum ImageFileExtension
{
    none = 0,
    jpg = 1,
    jpeg = 2,
    bmp = 3,
    gif = 4,
    png = 5
}
public enum FileType
{
    Image = 1,
    Video = 2,
    PDF = 3,
    Text = 4,
    DOC = 5,
    DOCX = 6,
    PPT = 7,
}

如果它通过了基本验证,那么我们将调用 isValidFileMethod,该方法接受字节、文件类型、FileContentType 作为输入。

public static bool IsValidFile(byte[] bytFile, FileType flType, String fileContentType)
 {
     bool isvalid = false;

     if (flType == FileType.Image)
     {
         isvalid = IsValidImageFile(bytFile, fileContentType);
     }
     else if (flType == FileType.Video)
     {
         isvalid = IsValidVideoFile(bytFile, fileContentType);
     }
     else if (flType == FileType.PDF)
     {
         isvalid = IsValidPdfFile(bytFile, fileContentType);
     }

     return isvalid;
 }

调用 isValidFile 方法后,它将根据文件类型调用另一个 static 方法。

如果文件类型为 image,则调用第一个方法 [isValidImageFile],否则文件类型为 Video,则调用第二个方法 [isValidVideoFile],同样,如果文件类型为 PDF,则调用最后一个方法 [isValidPDFFile]。

在完成对 isValidFileMethod 的理解后,让我们来看一下将要调用的 [isValidImageFile] 方法。

以下是 [isValidImageFile] 方法的完整代码片段。

在此方法中,我们允许有限的图像文件扩展名 [jpg, jpeg, png, bmp, gif]。

使用此 isValidImageFile 方法。

当我们向此方法传递字节和 FileContentType 时,它将首先根据 FileContentType 进行检查,根据该检查,它将设置 ImageFileExtension,然后它将检查我们上传文件的文件头字节是否与上传文件的字节匹配,如果匹配,则文件有效 [true],否则文件无效 [false]。

public static bool IsValidImageFile(byte[] bytFile, String fileContentType)
{
    bool isvalid = false;

    byte[] chkBytejpg = { 255, 216, 255, 224 };
    byte[] chkBytebmp = { 66, 77 };
    byte[] chkBytegif = { 71, 73, 70, 56 };
    byte[] chkBytepng = { 137, 80, 78, 71 };

    ImageFileExtension imgfileExtn = ImageFileExtension.none;

    if (fileContentType.Contains("jpg") | fileContentType.Contains("jpeg"))
    {
        imgfileExtn = ImageFileExtension.jpg;
    }
    else if (fileContentType.Contains("png"))
    {
        imgfileExtn = ImageFileExtension.png;
    }
    else if (fileContentType.Contains("bmp"))
    {
        imgfileExtn = ImageFileExtension.bmp;
    }
    else if (fileContentType.Contains("gif"))
    {
        imgfileExtn = ImageFileExtension.gif;
    }

    if (imgfileExtn == ImageFileExtension.jpg || imgfileExtn == ImageFileExtension.jpeg)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 3; i++)
            {
                if (bytFile[i] == chkBytejpg[i])
                {
                    j = j + 1;
                    if (j == 3)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }

    if (imgfileExtn == ImageFileExtension.png)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 3; i++)
            {
                if (bytFile[i] == chkBytepng[i])
                {
                    j = j + 1;
                    if (j == 3)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }

    if (imgfileExtn == ImageFileExtension.bmp)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 1; i++)
            {
                if (bytFile[i] == chkBytebmp[i])
                {
                    j = j + 1;
                    if (j == 2)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }

    if (imgfileExtn == ImageFileExtension.gif)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 1; i++)
            {
                if (bytFile[i] == chkBytegif[i])
                {
                    j = j + 1;
                    if (j == 3)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }

    return isvalid;
}

从 Action Method 调用 isValidFile 方法

我们将调用 (FileUploadCheck.isValidFile) 方法,然后传递参数文件字节、类型、FileContentType

此方法将返回布尔值,如果文件有效则返回 true,否则返回 false

byte[] tempFileBytes = null;

 // getting File Name
 var fileName = file.FileName.Trim();

 using (BinaryReader reader = new BinaryReader(file.OpenReadStream()))
 {
     // getting filebytes
     tempFileBytes = reader.ReadBytes((int)file.Length);
 }

 // Creating new FileName
 var myUniqueFileName = Convert.ToString(Guid.NewGuid());

 var filetype = Path.GetExtension(fileName).Replace('.', ' ').Trim();

 // getting FileExtension
 var fileExtension = Path.GetExtension(fileName);

 var types = CoreSecurity.Filters.FileUploadCheck.FileType.Image;  // Setting Image type
 var result = FileUploadCheck.IsValidFile(tempFileBytes, types, filetype); // Validate Header

理解了代码片段后,现在让我们来看一个演示。

下面的快照显示了一个带有文件上传控件的 Employee 表单。

我们将填写以下表单并选择一个有效文件。

图 33. 文件上传。

上传 Index Post Action 方法的调试快照

在这部分,我们发布了一个带有文件的表单。这里,我们可以看到它如何进行基本验证。

图 34. 上传图像后调试模式。

调试 Index Post Action 方法的快照

在这部分,您可以查看我们上传文件的实时值。它已通过基本验证。

调用 isVaildFile 方法时 FileUploadCheck 类的快照

在这部分,调用 isValidFile 方法后,它将根据其 FileContentType 调用另一个方法。

检查文件头字节时的 isValidImageFile 方法快照

在此方法中,它将检查上传图像的文件头字节与我们拥有的字节是否匹配,如果匹配,则文件有效,否则无效。

图 35. 检查字节头。

5. 安全配置错误(必须设置自定义错误页面)

当我们开发 Web 应用程序时,最终的输出始终是 HTML,对吧?但是,这会在最终用户那里下载,如果用户稍微聪明一些,他们就会玩弄 HTML 标签并尝试在客户端更改值并将其发布到服务器。这就是为什么同时进行客户端和服务器端验证始终是强制性的。

那么,让我们实际演示一下。

示例

为了展示演示,我创建了一个 Employee 表单,该表单接受基本的 Employee 详细信息。

图 36. 注册表单。

注册模型视图

public class Registration
    {
        public int? RegistrationId { get; set; }

        [Required(ErrorMessage = "Enter FirstName")]
        [StringLength(50, ErrorMessage = "Only 50 Characters are Allowed")]
        public string FirstName { get; set; }

        [StringLength(50, ErrorMessage = "Only 50 Characters are Allowed")]
        [Required(ErrorMessage = "Enter LastName")]
        public string LastName { get; set; }

        [EmailAddress(ErrorMessage = "Invalid Email Address")]
        [Required(ErrorMessage = "Enter EmailId")]
        public string EmailId { get; set; }

        [Required(ErrorMessage = "Enter Username")]    
        public string Username { get; set; }

        [Required(ErrorMessage = "Enter Password")]
        public string Password { get; set; }

        [NotMapped]
        [Required(ErrorMessage = "Enter ConfirmPassword")]
        [Compare("Password", ErrorMessage = "Password does not match")]
        public string ConfirmPassword { get; set; }


        public DateTime? CreatedDate { get; set; }
        public DateTime? UpdateDate { get; set; }
        public bool? Status { get; set; }
    }

那么,我们的数据注释验证是否足以保护页面?不,这还不足以保护页面,我将向您展示一个关于这些验证如何被绕过的简短演示。

下面的快照显示 FirstName 字段正在验证。它只要求 50 个字符。

图 37. 在为模型添加验证后,现在我们正在验证表单,您可以看到 FirstName 输入字段只接受 50 个字符,它不接受超过 50 个字符,它会显示错误和消息。

拦截添加注册视图

现在让我们拦截此表单,然后从拦截处将其提交到服务器。我正在使用一个名为 burp suit 的工具,它可以捕获发送到服务器和来自服务器的请求。

在下面的快照中,我捕获了一个发送到服务器的请求。

图 38. 使用 burp suite 拦截添加注册表单,您可以查看用户提交表单时,该工具会捕获数据。

在下面的快照中,我捕获的请求正在发送到服务器。您可以看到我更改了 FirstName 字段,它只接受 50 个字符,我添加了超过 50 个字符然后提交给服务器。

拦截添加注册表单的 FirstName 字段

图 39. 在 burp suite 中拦截 FirstName 字段并提交到服务器。

下面是显示发送到服务器的请求的快照,该请求包含超过 50 个字符。

注册表单的调试模式

FirstName 字段提交超过 50 个字符后,服务器会抛出异常。因为在数据库中,FirstName 字段的数据类型是 varchar(50),而数据超过 50 个字符,所以异常是显而易见的。

图 40. 在 burp suite 中拦截 FirstName 字段并提交到服务器。

图 41. 拦截的 FirstName 字段导致了错误。

显示给用户错误的问

现在抛出的异常直接显示给攻击者,这会泄露大量关于服务器和我们的程序行为的宝贵信息。利用这些错误信息,他可以尝试各种排列组合来利用我们的系统。

图 42. 直接向用户显示错误。

因此,这里的解决方案是我们需要设置某种错误页面,该页面不显示内部技术错误,而是显示自定义错误消息。

我们有两种方法:

  • 创建自定义错误处理属性
  • 设置自定义错误页面

解决方案 1

使用 ExceptionFilterAttribute 创建自定义错误处理属性。

此属性将处理异常并根据日期将异常写入文本文件,它存储在 wwwroot 文件夹中 -> ErrorLogPath

如果您还将此异常存储在数据库中,那么您只需要添加一个表并编写 ADO.NET 代码来插入数据。

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly IHostingEnvironment _hostingEnvironment;

    public CustomExceptionFilterAttribute(IHostingEnvironment hostingEnvironment)
    {
        _hostingEnvironment = hostingEnvironment;
    }

    public override void OnException(ExceptionContext context)
    {
        string strLogText = "";
        Exception ex = context.Exception;

        context.ExceptionHandled = true;
        var objClass = context;
        strLogText += "Message ---\n{0}" + ex.Message;

        if (context.HttpContext.Request.Headers["x-requested-with"] == "XMLHttpRequest")
        {
            strLogText += Environment.NewLine + ".Net Error ---\n{0}" + 
                          "Check MVC Ajax Code For Error";
        }

        strLogText += Environment.NewLine + "Source ---\n{0}" + ex.Source;
        strLogText += Environment.NewLine + "StackTrace ---\n{0}" + ex.StackTrace;
        strLogText += Environment.NewLine + "TargetSite ---\n{0}" + ex.TargetSite;
        if (ex.InnerException != null)
        {
            strLogText += Environment.NewLine + "Inner Exception is {0}" + ex.InnerException;
            //error prone
        }
        if (ex.HelpLink != null)
        {
            strLogText += Environment.NewLine + "HelpLink ---\n{0}" + 
                          ex.HelpLink;//error prone
        }

        StreamWriter log;

        string timestamp = DateTime.Now.ToString("d-MMMM-yyyy", new CultureInfo("en-GB"));

        string errorFolder = Path.Combine(_hostingEnvironment.WebRootPath, "ErrorLog");

        if (!System.IO.Directory.Exists(errorFolder))
        {
            System.IO.Directory.CreateDirectory(errorFolder);
        }

        // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
        if (!File.Exists(String.Format(@"{0}\Log_{1}.txt", errorFolder, timestamp)))
        {
            log = new StreamWriter(String.Format(@"{0}\Log_{1}.txt", errorFolder, timestamp));
        }
        else
        {
            log = File.AppendText(String.Format(@"{0}\Log_{1}.txt", errorFolder, timestamp));
        }

        var controllerName = (string)context.RouteData.Values["controller"];
        var actionName = (string)context.RouteData.Values["action"];

        // Write to the file:
        log.WriteLine(Environment.NewLine + DateTime.Now);
        log.WriteLine
        ("------------------------------------------------------------------------------------------------");
        log.WriteLine("Controller Name :- " + controllerName);
        log.WriteLine("Action Method Name :- " + actionName);
        log.WriteLine
        ("------------------------------------------------------------------------------------------------");
        log.WriteLine(objClass);
        log.WriteLine(strLogText);
        log.WriteLine();

        // Close the stream:
        log.Close();
        context.HttpContext.Session.Clear();

        if (!_hostingEnvironment.IsDevelopment())
        {
            // do nothing
            return;
        }
        var result = new RedirectToRouteResult(
        new RouteValueDictionary
        {
            {"controller", "Errorview"}, {"action", "Error"}
        });
        // TODO: Pass additional detailed data via ViewData
        context.Result = result;
    }
}

创建 CustomExceptionFilterAttribute 后,接下来我们将全局注册此过滤器。

图 43. 全局注册 CustomExceptionFilterAttribute 过滤器。

每当发生错误时,CustomExceptionFilter 属性将被调用,它将重定向到 Errorview 控制器和 Error Action 方法。

图 44. 应用程序中发生任何错误时显示自定义错误页面。

发生的异常也存储在 wwwroot 文件夹内的 ErrorLog 文件夹中。

图 45. 显示存储在错误日志文件夹中的错误。

解决方案 2

ASP.NET Core 有三个环境

  1. 开发
  2. 暂存
  3. 生产

在将应用程序部署到生产环境之前,我们应将托管环境设置为“Production”。

接下来,我们将配置 UseExceptionHandler 中间件。在此之前,我们可以通过 env.IsProduction() 方法检查我们的托管环境名称,如果设置为“Production”,则 if 语句将执行。

图 46. 配置异常处理器。

在其中,我们将设置“UseExceptionHandler”中间件,您可以在其中为生产应用程序设置错误处理路径。

如果发生任何错误,它将调用“Home”控制器和 Error Action 方法,该方法将显示错误视图。

图 47. 开发环境中的异常页面。

注意:仅在应用程序处于开发环境运行时才启用开发人员异常页面。

6. 版本泄露

应用程序开发的版本不应向最终用户透露,因为如果攻击者获得应用程序的特定版本,他可能会尝试针对该已泄露的版本进行特定攻击。

每当浏览器向服务器发送 HTTP 请求时,我们都会收到响应标头,其中包含 [服务器、X-Powered-By、X-SourceFiles] 信息。

服务器显示有关正在使用哪个 Web 服务器的信息。

Server: Kestrel: 由 Kestrel 托管的应用程序

X-Powered-By: ASP.NET: 显示您的网站运行的框架信息。

注意

X-SourceFiles 标头仅为 localhost 请求生成,用于 Visual Studio 和 IIS Express 的调试目的。

图 48. 响应标头泄露版本信息。

解决方案

  1. 移除 Server: Kestrel 标头
  2. 移除 X-Powered-By: ASP.NET 标头
  3. 从 NuGet 添加 NWebsec.AspNetCore.Middleware 以保护标头

移除 Server: Kestrel 标头

要移除 Server: Kestrel 标头,您需要设置“AddServerHeader = false”。

代码片段

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace CoreSecurity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseKestrel(c => c.AddServerHeader = false)
                .UseStartup<Startup>()
                .Build();
    }
}

图 49. 移除 Server 标头。

以下是移除 Server: Kestrel 标头后的快照。

图 50. 移除 Server 标头后的响应。

移除 X-Powered-By: ASP.NET 标头

要移除此标头,我们需要向项目添加一个 web.config 文件。

图 51. 向项目添加 web.config 文件。

在添加 web.config 文件后,我们将向 <system.webServer> 元素下的 <httpProtocol> 元素添加,以移除“X-Powered-By 标头。

新添加时的 web.config 文件代码片段

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!-- To customize the asp.net core module uncomment and edit the following section. 
  For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->
  <system.webServer>
    <handlers>
      <remove name="aspNetCore"/>
      <add name="aspNetCore" path="*" verb="*" 
      modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" 
    stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>

在 <system.webServer> 下添加的元素

<httpProtocol>
  <customHeaders>
    <remove name="X-Powered-By" />
  </customHeaders>
</httpProtocol>

在 <system.webServer> 下添加 <httpProtocol> 元素后的 web.config 文件代码片段

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <!-- To customize the asp.net core module uncomment and edit the following section. 
  For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->
  
  <system.webServer>
    <handlers>
      <remove name="aspNetCore"/>
      <add name="aspNetCore" path="*" verb="*" 
      modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" 
    stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

以下是移除“X-Powered-By 标头后的快照。

图 52. 移除“X-Powered-By” 标头后的快照。

从 NuGet 添加 NWebsec.AspNetCore.Middleware 以保护标头

NWebsec 是 ASP.NET Core 应用程序的中间件。

NWebsec 帮助您设置重要的安全标头并检测潜在的危险重定向。

NWebsec 防止跨站脚本、iframe 并防止点击劫持攻击。

图 53. 从 NuGet 安装 NWebsec.AspNetCore.Middleware 以保护标头。

要在 Startup 类中的 Configure 方法中添加的代码片段

// X-Content-Type-Options header
app.UseXContentTypeOptions();
// Referrer-Policy header.
app.UseReferrerPolicy(opts => opts.NoReferrer());
// X-Xss-Protection header
app.UseXXssProtection(options => options.EnabledWithBlockMode());
// X-Frame-Options header
app.UseXfo(options => options.Deny());
// Content-Security-Policy header
app.UseCsp(opts => opts
    .BlockAllMixedContent()
    .StyleSources(s => s.Self())
    .StyleSources(s => s.UnsafeInline())
    .FontSources(s => s.Self())
    .FormActions(s => s.Self())
    .FrameAncestors(s => s.Self())
    .ImageSources(s => s.Self())
    .ScriptSources(s => s.Self())
);

图 54. 配置 NWebsec.AspNetCore 中间件。

以下是添加保护标头后的快照。

图 55. 添加响应标头以防止点击劫持和跨站脚本 (XSS) 攻击。

引用于: https://damienbod.com/2018/02/08/adding-http-headers-to-improve-security-in-an-asp-net-mvc-core-application/

7. 跨站请求伪造 (CSRF)

跨站请求伪造 (CSRF) 是一种攻击,它迫使最终用户在他们已成功登录的 Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态更改请求,而不是数据窃取,因为攻击者无法看到伪造请求的响应。通过一点社交工程(例如通过电子邮件或聊天发送链接),攻击者可能会欺骗 Web 应用程序的用户执行攻击者选择的操作。如果受害者是普通用户,成功的 CSRF 攻击可能会迫使用户执行状态更改请求,例如转账、更改他们的电子邮件地址等。如果受害者是管理员帐户,CSRF 可能会危及整个 Web 应用程序。

定义引用自: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

举个简单的例子

用户登录银行服务器。

银行授权并在用户和银行服务器之间建立安全会话。

攻击者向用户发送一封包含恶意链接的电子邮件,说“立即赚取 100000$”。

用户点击恶意链接,该网站会尝试将您的帐户中的钱转入攻击者的帐户。

由于建立了安全会话,恶意代码可以成功执行。

Microsoft 已经认识到这种威胁,为了预防它,我们有称为 AntiForgeryToken 的东西。

解决方案

我们需要在表单 HTML 标签上添加 asp-antiforgery="true" HTML 属性并将其属性设置为 true 以生成反伪造。默认情况下,它是 false,这意味着它不会生成反伪造令牌。在我们处理 POST ([HttpPost]) 请求的操作方法上,我们需要添加 [ValidateAntiForgeryToken] 属性,该属性将检查令牌是否有效。

图 56. 在表单标签上设置反伪造属性。

[ValidateAntiForgeryToken] 属性添加到 [HttpPost] 方法。

AntiForgeryToken 的工作原理

当我们在表单帮助标签上将反伪造令牌设置为 true (asp-antiforgery="true") 时,它会创建一个隐藏字段并为其分配一个唯一的令牌值,同时在浏览器中添加一个会话 Cookie。

当我们发布表单 HTML 时,它会检查 __RequestVerificationToken 隐藏字段以及 __RequestVerificationToken Cookie 是否存在。如果 Cookie 或表单 __RequestVerificationToken 隐藏字段值丢失,或者值不匹配,ASP.NET MVC 将不会处理该操作。这就是我们可以在 ASP.NET MVC 中防止跨站请求伪造攻击的方法。

图 56. RequestVerificationToken 生成的隐藏字段。

RequestVerificationToken Cookie 快照。

图 57. RequestVerificationToken 生成 Cookie。

8. XML 外部实体 (XXE)

此攻击针对解析 XML 的应用程序。我们现在消耗各种返回 XML 的 Web 服务,如果我们不检查我们收到的响应,它可能会导致攻击发生。

  1. XML 文件中说明的“十亿笑话”攻击
  2. 外部实体 攻击

注意:文档类型定义 (DTD)

十亿笑话攻击

图 58. 十亿笑话攻击 XML

这种攻击会导致拒绝服务 (DoS) 攻击,使您的服务器利用率过高,导致服务器关机。

XML 文件中说明的“十亿笑话”攻击

<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
图 59. 十亿笑话攻击 XML。

为了演示,我添加了一个名为 XXE 的控制器,其中包含 index 操作方法。

在 index 操作方法中,我将从“XmlFiles”文件夹消耗一个 XML,其中包含恶意 XML(“十亿笑话攻击 XML”),然后我们将使用“XmlTextReader”来解析 XML。

CodeProject

[HttpGet]
public IActionResult Index()
{
    var fileName = Path.Combine(_environment.WebRootPath, "XmlFiles") + "\\temp.xml";
    XmlTextReader reader = new XmlTextReader(fileName);
    reader.DtdProcessing = DtdProcessing.Parse;
    while (reader.Read())
    {
        var data = reader.Value;
    }
    return View();
}

如果您运行此示例,您会看到您的系统将具有很高的利用率,请自行承担风险。

此攻击的解决方案

为避免此类攻击,我们可以将“XmlTextReader”的 DtdProcessing 属性设置为 Prohibit 或 Ignore。

  • Prohibit – 如果识别到 DTD,则抛出异常
  • Ignore – 忽略文档中的任何 DTD 规范,跳过它们并继续处理文档
  • Parse (默认) – 将解析文档中的任何 DTD 规范。(可能存在漏洞)

CodeProject

  [HttpGet]
  public IActionResult Index()
  {
      var fileName = Path.Combine(_environment.WebRootPath, "XmlFiles") + "\\temp.xml";
      XmlTextReader reader = new XmlTextReader(fileName);
      reader.DtdProcessing = DtdProcessing.Ignore;
      while (reader.Read())
      {
          var data = reader.Value;
      }

      return View();
  }

外部实体攻击

此攻击中,恶意 XML 的配置方式使其能够访问服务器上的文件,此攻击来自外部源,因此命名为 XML 外部实体。

为了演示,我在我的 D 驱动器上存储了一个名为 demo.txt 的文本文件。

现在我将尝试使用恶意 XML 从此访问文件。

XML 中说明的外部实体攻击

<?xml version=\"1.0\"?><!DOCTYPE doc 
[<!ENTITY win SYSTEM \"file:///D:/demo.txt\">]><doc>&win;</doc>

代码片段

using System;
using System.IO;
using System.Xml;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string xml = "<?xml version=\"1.0\"?><!DOCTYPE doc 
            [<!ENTITY win SYSTEM \"file:///D:/demo.txt\">]><doc>&win;</doc>";

            XmlReaderSettings rs = new XmlReaderSettings();
            rs.DtdProcessing = DtdProcessing.Parse;

            XmlReader myReader = XmlReader.Create(new StringReader(xml), rs);

            while (myReader.Read())
            {
                Console.WriteLine(myReader.Value);
            }
            Console.ReadLine();
        }
    }
}

图 60. 执行恶意 XML 后的输出。

此攻击的解决方案

为避免此类攻击,我们可以将“XmlTextReader”的 DtdProcessing 属性设置为 ProhibitIgnore

设置此属性后,它将不允许解析 XML。

图 61. 设置 DtdProcessing 属性为 Prohibit 后出错。

9. 不安全的反序列化

在了解不安全的反序列化之前,让我们先了解一下序列化和反序列化过程?

序列化是将对象转换为字节流的任务,以便它可以被存储或传输。

反序列化是序列化的反向操作,即将字节流再次转换为对象。

在现代 Web 开发时代,我们经常使用 JSON 数据和 XML 数据进行序列化和反序列化。但是序列化过程没有问题,因为我们在应用程序端进行该过程,但反序列化过程我们从可信和不可信的源接收文件或字节流,这会导致问题。

我们经常使用数据库、缓存服务器、文件系统来存储序列化数据,并

  1. 拒绝服务 (DoS) 攻击
  2. 远程代码执行攻击
  3. 数据篡改或访问控制

图 62. 序列化和反序列化过程。

受影响的库快照

James Forshaw 的《Breaking .NET Through Serialization》白皮书 @contextis.com

此快照来源文档的链接。

https://media.blackhat.com/bh-us-12/Briefings/Forshaw/BH_US_12_Forshaw_Are_You_My_Type_WP.pdf

图 63. 受影响的库。

反序列化示例

我在 ASP.NET Core 中创建了一个名为“stock”的简单 API,该 API 根据消费者发送的 company name (name) 参数从数据库获取股票数据,并以响应形式发送该公司的股票数据。

有效请求

请求 URL: https://:57777/api/Stock

请求 Json

{'Stockid':"1",'Name':"GComp1"}

图 64. 发布有效请求。发送请求后的调试视图。

图 65. 发布值后的调试视图。

无效请求

请求 URL: https://:57777/api/Stock

请求 Json

{'Stockid':"1",'Name':" 'or'1'='1 "}

图 66. 发布无效请求(恶意请求)。

发送请求后的调试视图。

图 67. 发布恶意值后的调试视图。

在此过程中,您可以看到我正在向 API 发送恶意 JSON 数据,这会导致 SQL 注入攻击。

此攻击的解决方案

  1. 如果您正在使用第三方 NuGet 扩展,请检查最新的更新,并检查它们是否对此类攻击有解决方案。
  2. 在反序列化(JSON 和 XML)之前检查您收到的输入数据。
  3. 为避免使用参数化查询或存储过程的 SQL 注入攻击,Entity Framework 也使用参数化查询。

使用参数化查询查询 SQL Server

图 68. 参数化查询。

10. SQL 注入攻击

过去三年中最常见的攻击是 SQL 注入攻击。因为每个应用程序都需要一个数据库来存储数据,而数据是有价值的。SQL 注入攻击可以为攻击者提供有价值的数据,这可能导致严重的安全漏洞,并且还可以完全访问数据库服务器。

在 SQL 注入中,攻击者总是尝试输入将要在数据库中执行的恶意 SQL 语句,并向攻击者返回不需要的数据。

图 69. SQL 注入攻击示例,显示如果您使用内联查询,攻击是如何发生的。

显示用户数据的简单视图

视图显示单个注册数据,基于 RegistrationId,如下面的快照所示。

图 70. 显示用户数据的注册视图。

SQL 注入攻击后显示所有用户数据的简单视图

在此浏览器视图中,作为攻击者看到了包含一些有价值数据的应用程序 URL,即 ID [https://:3837/demo/index?Id=1],攻击者尝试 SQL 注入攻击,如下所示。

图 71. SQL 注入后显示所有用户数据的注册视图。

在尝试了 SQL 注入攻击的排列和组合后,攻击者获得了所有用户数据的访问权限。

在调试模式下显示 SQL 注入

在这里,我们可以详细了解攻击者如何传入将在数据库中执行的恶意 SQL 语句。

图 72. 调试模式显示执行中的恶意 SQL 语句。

SQL 语句的 SQL Profiler 视图

图 73. 执行 SQL 语句后的 SQL Profiler 视图。

解决方案

  1. 验证输入
  2. 使用低权限的数据库登录
  3. 使用参数化查询
  4. 使用 ORM(例如 Dapper、Entity framework)
  5. 使用存储过程

验证输入

始终在客户端和服务器端都验证输入,这样就不会在输入中输入特殊字符,这会让攻击者有机可乘。

在 MVC 中,我们使用数据注释进行验证。

数据注释属性是应用于模型以验证模型数据的简单规则。

图 74. 应用正则表达式后的视图。

服务器端验证输入

模型状态为 false - 这表示模型无效。

使用低权限的数据库登录

Db_owner 是数据库的默认角色,它可以授予和撤销访问权限、创建表、存储过程、视图、运行备份、安排作业,甚至可以删除数据库。如果将此类角色访问权限授予数据库,那么正在使用的用户就可以拥有完全访问权限,并且他可以对其执行各种活动。我们必须创建一个具有最低权限的新用户帐户,并仅授予该用户必须访问的权限。

例如,如果用户需要处理与选择、插入和更新员工详细信息相关的任务,那么应仅提供 selectInsertupdate 权限。

图 75. 创建新用户并分配对象访问权限。

添加新用户并提供权限的步骤

选择表后,我们将为该表提供权限,如下所示,您可以看到我们只授予了该用户对“Insert”、“Select”、“Update”的权限。

图 76. 分配权限。

拥有最低权限,将有助于我们保护数据库免受攻击。

使用存储过程

存储过程是参数化查询的一种形式。使用存储过程也是一种防止 SQL 注入攻击的方法。

在下面的快照中,我们删除了之前使用的内联查询。之前,我们已经编写了通过存储过程从数据库获取数据的代码,这有助于我们防止 SQL 注入攻击,然后对于存储过程,我们需要传递参数 [@RegistrationID],并根据参数,它将从数据库中获取记录,在传递参数后,我们使用了 CommandType [CommadType.StoredProcedure],这表明我们正在使用存储过程。

注意:始终将参数与存储过程一起使用,如果您不使用它,您仍然容易受到 SQL 注入攻击。

图 77. 使用存储过程。

使用存储过程后,让我们请求演示视图。

显示记录的注册视图

图 78. 根据 Registration ID 显示用户详细信息的注册视图。

使用存储过程后的 Profiler 视图

显示我们使用的存储过程的跟踪。

图 79. 执行 SQL 语句后的 SQL Profiler 视图。

使用存储过程,让我们再次尝试 SQL 注入攻击。

使用存储过程后,在调试模式下显示 SQL 注入攻击执行情况

如果您查看参数 id [?Id=1or 1=1],其中包含恶意 SQL 脚本,在将其传递给存储过程后,它会显示一个错误,表明它没有被执行,因为我们传递的参数是整数,只接受数字作为输入,如果我们传递恶意 SQL 脚本 [?Id=1 or 1=1],它会抛出错误。

使用存储过程后的 Profiler 视图

图 80. 执行 SQL 语句后的 SQL Profiler 视图。

输出

图 81. 执行恶意 SQL 脚本后显示错误页面。

使用参数化查询

使用参数化查询是防止 SQL 注入攻击的另一种解决方案。它与存储过程类似。除了连接字符串,您需要将参数添加到 SQL 查询中,并使用 SqlCommand 传递参数值。

图 81. 执行 SQL 注入攻击。

图 82. 执行恶意 SQL 脚本后发生错误。

参数化查询的 Profiler 视图

图 83. 执行 SQL 语句后的 SQL Profiler 视图

输出

图 84. 执行恶意 SQL 脚本后显示错误页面。

使用 ORM(实体框架)

ORM 代表对象关系映射器,它将 SQL 对象映射到您的域对象 [C#]。

如果您正确使用实体框架,您就不容易受到 SQL 注入攻击,因为实体框架内部使用参数化查询。

实体框架的正常执行

在这里,我们已将姓名作为参数 [?name=Saineshwar] 传递。

图 85. 从查询字符串传递 firstName 值。

执行期间的控制器快照

在调试模式下,我们可以看到我们从 Querystring 传递了 name 参数,然后将其传递给 LINQ 查询以获取记录。

图 86. 调试视图传递来自查询字符串的 firstName 值。

LINQ 查询的 Profiler 视图

实体框架内部使用参数化查询,这是真的,这里有一个快照。

图 87. 执行 SQL 语句后的 SQL Profiler 视图。

尝试对实体框架进行 SQL 注入攻击

在这部分,我们正在尝试对实体框架进行 SQL 注入攻击。为此,我们传递了参数 [?firstName=Saineshwar or 1=1]。

图 88. 对实体框架进行 SQL 注入攻击。

执行期间的控制器快照

在调试模式下,我们可以看到我们从 Querystring 传递了 name 参数,然后将其传递给 LINQ 查询以获取记录,但这次,它还包含一个恶意脚本以及正常的参数。

图 89. 对实体框架进行 SQL 注入攻击。

LINQ 查询的 Profiler 视图

如果仔细查看跟踪,它将名称和恶意脚本视为一个参数,从而阻止了 SQL 注入攻击。

图 90. 执行 SQL 语句后的 SQL Profiler 视图。

结论

在本文中,我们已按照步骤学习了如何保护 ASP.NET Core MVC 应用程序免受 OWSAP(开放 Web 应用程序安全项目)列出的十大攻击。

感谢您的阅读。希望您喜欢我的文章。

历史

  • 2018 年 9 月 5 日:初始版本
© . All rights reserved.