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






4.99/5 (31投票s)
如何保护 ASP.NET Core MVC 应用程序免受十大攻击
引言
我们是 .NET Core 框架的新手,并且正在使用它来开发生产应用程序,但在开发生产应用程序时,我们还必须考虑安全性。因此,在本文中,我们将介绍 10 个有助于我们保护 ASP.NET Core MVC 代码安全的要点。
目录
- 身份验证和会话管理失效
- 敏感数据泄露与审计日志
- 跨站脚本 (XSS) 攻击
- 恶意文件上传
- 安全配置错误(必须设置自定义错误页面)
- 版本泄露
- 跨站请求伪造 (CSRF)
- XML 外部实体 (XXE)
- 不安全的反序列化
- SQL 注入攻击
- 结论
- 历史
1. 身份验证和会话管理失效
在这一部分,如果我们未能妥善管理应用程序的身份验证,攻击者可能会窃取用户凭据,例如会话、Cookie,这可能允许攻击者完全访问整个应用程序,然后他们可能会尝试访问应用程序服务器和数据库服务器,从而导致大规模数据泄露。
攻击者可能窃取数据的方式
- 不安全连接(未使用的 SSL)
- 可预测的登录凭据
- 未以加密形式存储凭据
- 应用程序登出不当
可能发生的攻击
会话固定
在找到预防此攻击的方法之前,让我们先来看一个会话固定攻击如何发生的简短演示。
每当用户向服务器发送第一个请求时,就会加载登录页面,然后用户输入有效的登录凭据以登录 Web 应用程序。成功登录后,我们在会话中分配一些值以识别唯一用户,同时在浏览器中添加一个 [".AspNetCore.Session"]
Cookie,用于识别发送请求的特定用户,并且 [".AspNetCore.Session"]
Cookie 值将始终发送到服务器,直到您从应用程序中登出为止。登出时,我们基本上会编写代码来删除创建的会话值,但我们不会删除登录时创建的 [".AspNetCore.Session"]
Cookie。此值有助于攻击者执行会话固定攻击。
用户输入有效凭据后
输入有效登录凭据后,[".AspNetCore.Session"]
Cookie 会被添加到浏览器。
注意:当任何数据保存到会话中时,[".AspNetCore.Session"]
Cookie 就会被创建并添加到用户浏览器。
从应用程序登出后 Cookie 仍然存在于浏览器中
从应用程序登出后,[".AspNetCore.Session"]
Cookie 仍然存在。
注意:预 Cookie 和后 Cookie 相同,这可能导致会话固定。
让我们进行一些会话固定演示
登出后未删除的 [".AspNetCore.Session"]
Cookie 有助于攻击者进行会话固定。我将打开一个浏览器(Chrome),在其中输入我们将要进行会话固定的应用程序的 URL [https://:53654/]。
在此视图中,我展示了用户登录时在 Firefox 浏览器中创建的 [".AspNetCore.Session"]
Cookie。
Firefox 浏览器中已创建的 Cookie。
注意:为了管理 Cookie,我在 Chrome 浏览器中安装了 Cookie Manager+ 插件。
在浏览器中输入 URL,现在让我们检查一下是否创建了 [".AspNetCore.Session"]
Cookie。哦,我们没有任何 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。
注意:为了添加 Cookie,我在 Chrome 浏览器中安装了 Edit this Cookie 插件。
固定 Cookie 后,现在我们不再需要登录应用程序,如果我们只需输入应用程序的内部 URL,就可以直接访问,因为此会话是在身份验证后创建的。
我的内部 URL 是:https://:53654/Dashboard/Index。
下面,您可以看到我无需登录应用程序即可访问仪表板页面,这是通过会话固定的。
解决方案
- 登出后移除
[".AspNetCore.Session"]
- 使用 SSL 保护 Cookie 和会话
- 通过设置 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
如果您查看下面的代码片段,我创建了一个名为 AuthenticateUser
的 AuthorizationFilter
,该过滤器继承了 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 过滤器
将此过滤器应用于用户登录应用程序后访问的每个控制器。
在所有用户登录后访问的控制器上应用操作过滤器。
现在,如果攻击者知道 [".AspNetCore.Session"]
Cookie 的值和新的 [Cookies["AuthenticationToken"]]
Cookie 的值,他们仍然无法进行会话固定攻击,因为新的 [Cookies["AuthenticationToken"]]
包含唯一且值相同的 GUID,并存储在 Web 服务器上的会话 [Session["AuthenticationToken"]]
中,但攻击者无法知道存储在 Web 服务器上的 Session
值,并且这些值在用户每次登录应用程序时都会变化,攻击者用来进行攻击的旧会话值将无法在此场景中使用。
最后,如果我们允许拥有有效 Session["AuthenticationToken"]
值和 Cookies["AuthenticationToken"]
值的用户访问应用程序。
两个 Cookie 的实时值
使用 SSL 保护 Cookie 和会话值
SSL(安全套接字层)是客户端和服务器之间进行安全(加密)通信的层,因此从客户端和服务器传递的任何数据(银行详细信息、密码、会话、Cookie 和其他金融交易)都是安全的(加密的)。
设置 Cookie HttpOnly 为 True
在 ASP.NET Core 中设置 HttpOnly Cookie。
HttpOnly 是在设置 Cookie 时可以用来阻止客户端脚本访问 Cookie 的一个标志。例如,JavaScript 无法读取已设置 HttpOnly 的 Cookie。
代码片段
- 创建
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);
- 在 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。
- 如果需要存储敏感数据,请使用强哈希技术。
保护登录
在保护之前,让我们看一下大多数开发人员创建的常见登录页面问题。
攻击者拦截登录页面以窃取凭据
当用户在登录页面输入凭据并提交到服务器时,数据[用户名和密码]以明文形式传输到服务器,此数据[用户名和密码]可以被攻击者拦截并窃取您的凭据。
始终使用带有种子的强哈希算法将敏感数据以加密格式发送到服务器
在此演示中,我们将使用 MD5 算法和种子在客户端加密数据,然后发送到服务器,以免被攻击者窃取。
- 我们将使用 MD5 算法,这是一种哈希算法。
- 此过程的第一步是生成一个随机数,然后生成一个作为种子的哈希值,该值将发送到客户端。
- 当用户输入用户名和密码并点击登录按钮时,在该时刻,我们生成 MD5 哈希,这是种子+密码的组合。
- 哈希后的密码在我们将表单发布到服务器时被接收,然后我们从数据库中按用户名获取哈希后的密码(不含种子),然后我们将密码(不含种子)与种子结合,并将收到的密码与种子进行比较,如果相等,则用户成功登录,否则显示错误消息。
拦截登录页面
在下面的快照中,当用户输入凭据并发布表单时。
拦截 POST 请求以显示密码如何以加密形式传输。
登录视图发布值后的登录操作方法
这里我们可以清楚地看到密码是加密形式的。
在下一步中,我们将比较存储在数据库中的密码。为此,首先,我们将通过用户输入的用户名获取密码,然后将其与数据库中存储的密码进行比较。绿色标记的行是种子值,同时,您可以看到标记为红色的数据库存储密码是我们通过传递用户名获得的,然后黄色行表示此密码是从登录视图发布的,最后蓝色行是数据库密码和种子的组合,我们将其与发布的密码进行比较。
使用 SSL 保护 Web 应用程序
SSL(安全套接字层)是用于客户端和服务器之间安全(加密)通信的层,因此[银行详细信息、密码和其他金融交易]等任何数据在客户端和服务器之间传递都是安全的(加密的)。
不要以明文形式在数据库中存储敏感数据
SQL Server 提供加密作为一项新功能来保护数据免受攻击者的攻击。攻击者可能能够访问数据库或表,但由于加密,他们将无法理解数据或使用它。
SQL Server 中加密数据的简单示例
我创建了一个简单的演示表,包含三列。第一列是 int
,第二列是 Varchar
,第三列是 VARBINARY
。
创建表后,我们创建了加密的“主密钥”,然后是“证书”和“对称密钥”,然后使用对称密钥加密数据。
下面的快照显示了我们如何加密“Encrycolumn
”列数据。
然后使用相同的密钥,我们将解密“Encrycolumn
”列数据。
以下是可根据需要用于数据加密和解密的算法列表。
哈希算法
如果有人只需要哈希,他们可以使用哈希算法,我们主要使用哈希函数来加密密码。
对称算法
如果有人只需要一个用于加密和解密的密钥,那么他们可以使用对称算法。
非对称算法
如果有人只需要一个用于加密(公钥)和一个用于解密(私钥)的密钥,那么他们可以使用非对称算法。例如,当我们将 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 表
查看生成的模型后,现在让我们创建一个名为 AuditFilter
的 ActionFilter
。
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
输出
用户请求页面并进行一些活动时插入到审计表中的数据。
3. 跨站脚本 (XSS) 攻击
跨站脚本 (XSS) 是一种通过输入字段注入恶意脚本的攻击。此攻击非常普遍,允许攻击者窃取凭据和有价值的数据,从而导致严重的安全漏洞。
执行此攻击的方式
- 来自输入
- 查询字符串
- HTTP 头
- 来自数据库的数据
为了演示,我创建了一个简单的表单,其中包含两个接受 HTML 作为输入的字段。
当你看到 JavaScript 被输入到文本字段时,第一个问题是什么?
这应该显示以下错误“检测到来自客户端的潜在危险的 Request.Form 值”。
注意
但在 ASP.NET Core 中,您不会收到此错误,因为 ASP.NET Core 2.0 不拒绝字段中的 HTML。
在发布该表单后,我们没有收到任何错误,并且值已在模型中接收,如下面的快照所示。
解决方案
[RegularExpressionAttribute]
- 使用 Razor 进行 HTML 编码
- URL 编码
XSS 攻击的第一个解决方案是使用正则表达式验证所有字段,这样只有有效数据才能通过。
1. [RegularExpressionAttribute]
使用正则表达式验证输入以防止 XSS 攻击,下方是快照。
应用正则表达式后,我们有了第一道防线,它不允许恶意输入。
要使用的正则表达式列表
- 字母和空格 [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 攻击脚本存储在数据库中,当我们在视图中渲染它时,这些脚本必须被执行。
当我将这些值显示在 Index 视图上时,它们不会被执行,因为 MVC 中使用的 Razor 引擎会自动编码所有输出值。
以下是来自 Razor 视图引擎的编码值,它们不允许脚本被执行。
现在我将向您展示,如果您不正确地编码此值会怎样。
如果我们不正确地编码值,这将允许执行恶意脚本。
3. URL 编码
我们主要使用 URL 通过查询字符串将数据从一个开放页面传输到另一个页面,但我们从不编码我们发送的数据,这也会导致 XSS 攻击。
要进行 HTML 编码、URL 编码,我们有内置的库,需要从 NuGet 安装。
在视图中对 URL 进行编码。
在视图中对 EmailId
字段进行编码后,接下来我们将使用开发人员工具查看编码字段的值。
显示如何对 URL 和 HTML 进行编码。
4. 恶意文件上传
我们通常通过应用客户端和服务端验证来保护我们的输入字段,但当涉及到文件上传控件时,我们会忽略验证,我说的对吗?在文件上传中,我们验证的一件事是文件扩展名,如果文件扩展名正确,我们就认为文件是有效的。但在实际场景中,事情并非如此。攻击者可以上传恶意文件,这可能会导致安全问题。攻击者可以更改文件扩展名(tuto.exe 为 tuto.jpeg),并且恶意脚本可以作为图像文件上传。大多数开发人员只查看文件的文件扩展名并将其保存在文件夹或数据库中,但文件扩展名是有效的,文件本身可能包含恶意脚本。
解决方案
- 我们需要做的第一件事是验证文件上传。
- 只允许访问所需的文件扩展名。
- 检查文件头。
我们在视图中添加了文件上传控件,接下来将在提交时验证文件。
在 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
的静态类。在此类中,有各种用于验证不同文件类型的方法,目前,我将向您展示如何验证图像文件并仅允许图像文件。
在上面的快照中,有一个 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
表单。
我们将填写以下表单并选择一个有效文件。
上传 Index Post Action 方法的调试快照
在这部分,我们发布了一个带有文件的表单。这里,我们可以看到它如何进行基本验证。
调试 Index Post Action 方法的快照
在这部分,您可以查看我们上传文件的实时值。它已通过基本验证。
调用 isVaildFile
方法时 FileUploadCheck
类的快照
在这部分,调用 isValidFile
方法后,它将根据其 FileContentType
调用另一个方法。
检查文件头字节时的 isValidImageFile 方法快照
在此方法中,它将检查上传图像的文件头字节与我们拥有的字节是否匹配,如果匹配,则文件有效,否则无效。
5. 安全配置错误(必须设置自定义错误页面)
当我们开发 Web 应用程序时,最终的输出始终是 HTML,对吧?但是,这会在最终用户那里下载,如果用户稍微聪明一些,他们就会玩弄 HTML 标签并尝试在客户端更改值并将其发布到服务器。这就是为什么同时进行客户端和服务器端验证始终是强制性的。
那么,让我们实际演示一下。
示例
为了展示演示,我创建了一个 Employee
表单,该表单接受基本的 Employee
详细信息。
注册模型视图
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 个字符。
拦截添加注册视图
现在让我们拦截此表单,然后从拦截处将其提交到服务器。我正在使用一个名为 burp suit 的工具,它可以捕获发送到服务器和来自服务器的请求。
在下面的快照中,我捕获了一个发送到服务器的请求。
在下面的快照中,我捕获的请求正在发送到服务器。您可以看到我更改了 FirstName
字段,它只接受 50 个字符,我添加了超过 50 个字符然后提交给服务器。
拦截添加注册表单的 FirstName 字段
下面是显示发送到服务器的请求的快照,该请求包含超过 50 个字符。
注册表单的调试模式
在 FirstName
字段提交超过 50 个字符后,服务器会抛出异常。因为在数据库中,FirstName
字段的数据类型是 varchar(50)
,而数据超过 50 个字符,所以异常是显而易见的。
显示给用户错误的问
现在抛出的异常直接显示给攻击者,这会泄露大量关于服务器和我们的程序行为的宝贵信息。利用这些错误信息,他可以尝试各种排列组合来利用我们的系统。
因此,这里的解决方案是我们需要设置某种错误页面,该页面不显示内部技术错误,而是显示自定义错误消息。
我们有两种方法:
- 创建自定义错误处理属性
- 设置自定义错误页面
解决方案 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
后,接下来我们将全局注册此过滤器。
每当发生错误时,CustomExceptionFilter
属性将被调用,它将重定向到 Errorview
控制器和 Error Action 方法。
发生的异常也存储在 wwwroot 文件夹内的 ErrorLog 文件夹中。
解决方案 2
ASP.NET Core 有三个环境
- 开发
- 暂存
- 生产
在将应用程序部署到生产环境之前,我们应将托管环境设置为“Production”。
接下来,我们将配置 UseExceptionHandler
中间件。在此之前,我们可以通过 env.IsProduction()
方法检查我们的托管环境名称,如果设置为“Production”,则 if
语句将执行。
在其中,我们将设置“UseExceptionHandler
”中间件,您可以在其中为生产应用程序设置错误处理路径。
如果发生任何错误,它将调用“Home”控制器和 Error Action 方法,该方法将显示错误视图。
注意:仅在应用程序处于开发环境运行时才启用开发人员异常页面。
6. 版本泄露
应用程序开发的版本不应向最终用户透露,因为如果攻击者获得应用程序的特定版本,他可能会尝试针对该已泄露的版本进行特定攻击。
每当浏览器向服务器发送 HTTP 请求时,我们都会收到响应标头,其中包含 [服务器、X-Powered-By、X-SourceFiles] 信息。
服务器显示有关正在使用哪个 Web 服务器的信息。
Server: Kestrel: 由 Kestrel 托管的应用程序
X-Powered-By: ASP.NET: 显示您的网站运行的框架信息。
注意
X-SourceFiles
标头仅为 localhost 请求生成,用于 Visual Studio 和 IIS Express 的调试目的。
解决方案
- 移除 Server: Kestrel 标头
- 移除 X-Powered-By: ASP.NET 标头
- 从 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();
}
}
以下是移除 Server: Kestrel 标头后的快照。
移除 X-Powered-By: ASP.NET 标头
要移除此标头,我们需要向项目添加一个 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
” 标头后的快照。
从 NuGet 添加 NWebsec.AspNetCore.Middleware
以保护标头
NWebsec
是 ASP.NET Core 应用程序的中间件。
NWebsec
帮助您设置重要的安全标头并检测潜在的危险重定向。
它 NWebsec
防止跨站脚本、iframe 并防止点击劫持攻击。
要在 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())
);
以下是添加保护标头后的快照。
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
] 属性,该属性将检查令牌是否有效。
将 [ValidateAntiForgeryToken]
属性添加到 [HttpPost]
方法。
AntiForgeryToken 的工作原理
当我们在表单帮助标签上将反伪造令牌设置为 true
(asp-antiforgery="true"
) 时,它会创建一个隐藏字段并为其分配一个唯一的令牌值,同时在浏览器中添加一个会话 Cookie。
当我们发布表单 HTML 时,它会检查 __RequestVerificationToken
隐藏字段以及 __RequestVerificationToken
Cookie 是否存在。如果 Cookie 或表单 __RequestVerificationToken
隐藏字段值丢失,或者值不匹配,ASP.NET MVC 将不会处理该操作。这就是我们可以在 ASP.NET MVC 中防止跨站请求伪造攻击的方法。
RequestVerificationToken Cookie 快照。
8. XML 外部实体 (XXE)
此攻击针对解析 XML 的应用程序。我们现在消耗各种返回 XML 的 Web 服务,如果我们不检查我们收到的响应,它可能会导致攻击发生。
- XML 文件中说明的“十亿笑话”攻击
- 外部实体 攻击
注意:文档类型定义 (DTD)
十亿笑话攻击
这种攻击会导致拒绝服务 (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>
为了演示,我添加了一个名为 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();
}
}
}
此攻击的解决方案
为避免此类攻击,我们可以将“XmlTextReader
”的 DtdProcessing
属性设置为 Prohibit
或 Ignore
。
设置此属性后,它将不允许解析 XML。
9. 不安全的反序列化
在了解不安全的反序列化之前,让我们先了解一下序列化和反序列化过程?
序列化是将对象转换为字节流的任务,以便它可以被存储或传输。
反序列化是序列化的反向操作,即将字节流再次转换为对象。
在现代 Web 开发时代,我们经常使用 JSON 数据和 XML 数据进行序列化和反序列化。但是序列化过程没有问题,因为我们在应用程序端进行该过程,但反序列化过程我们从可信和不可信的源接收文件或字节流,这会导致问题。
我们经常使用数据库、缓存服务器、文件系统来存储序列化数据,并
- 拒绝服务 (DoS) 攻击
- 远程代码执行攻击
- 数据篡改或访问控制
受影响的库快照
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
反序列化示例
我在 ASP.NET Core 中创建了一个名为“stock”的简单 API,该 API 根据消费者发送的 company name (name) 参数从数据库获取股票数据,并以响应形式发送该公司的股票数据。
有效请求
请求 URL: https://:57777/api/Stock
请求 Json
{'Stockid':"1",'Name':"GComp1"}
无效请求
请求 URL: https://:57777/api/Stock
请求 Json
{'Stockid':"1",'Name':" 'or'1'='1 "}
发送请求后的调试视图。
在此过程中,您可以看到我正在向 API 发送恶意 JSON 数据,这会导致 SQL 注入攻击。
此攻击的解决方案
- 如果您正在使用第三方 NuGet 扩展,请检查最新的更新,并检查它们是否对此类攻击有解决方案。
- 在反序列化(JSON 和 XML)之前检查您收到的输入数据。
- 为避免使用参数化查询或存储过程的 SQL 注入攻击,Entity Framework 也使用参数化查询。
使用参数化查询查询 SQL Server
10. SQL 注入攻击
过去三年中最常见的攻击是 SQL 注入攻击。因为每个应用程序都需要一个数据库来存储数据,而数据是有价值的。SQL 注入攻击可以为攻击者提供有价值的数据,这可能导致严重的安全漏洞,并且还可以完全访问数据库服务器。
在 SQL 注入中,攻击者总是尝试输入将要在数据库中执行的恶意 SQL 语句,并向攻击者返回不需要的数据。
显示用户数据的简单视图
视图显示单个注册数据,基于 RegistrationId
,如下面的快照所示。
SQL 注入攻击后显示所有用户数据的简单视图
在此浏览器视图中,作为攻击者看到了包含一些有价值数据的应用程序 URL,即 ID [https://:3837/demo/index?Id=1],攻击者尝试 SQL 注入攻击,如下所示。
在尝试了 SQL 注入攻击的排列和组合后,攻击者获得了所有用户数据的访问权限。
在调试模式下显示 SQL 注入
在这里,我们可以详细了解攻击者如何传入将在数据库中执行的恶意 SQL 语句。
SQL 语句的 SQL Profiler 视图
解决方案
- 验证输入
- 使用低权限的数据库登录
- 使用参数化查询
- 使用 ORM(例如 Dapper、Entity framework)
- 使用存储过程
验证输入
始终在客户端和服务器端都验证输入,这样就不会在输入中输入特殊字符,这会让攻击者有机可乘。
在 MVC 中,我们使用数据注释进行验证。
数据注释属性是应用于模型以验证模型数据的简单规则。
服务器端验证输入
模型状态为 false
- 这表示模型无效。
使用低权限的数据库登录
Db_owner
是数据库的默认角色,它可以授予和撤销访问权限、创建表、存储过程、视图、运行备份、安排作业,甚至可以删除数据库。如果将此类角色访问权限授予数据库,那么正在使用的用户就可以拥有完全访问权限,并且他可以对其执行各种活动。我们必须创建一个具有最低权限的新用户帐户,并仅授予该用户必须访问的权限。
例如,如果用户需要处理与选择、插入和更新员工详细信息相关的任务,那么应仅提供 select
、Insert
和 update
权限。
添加新用户并提供权限的步骤
选择表后,我们将为该表提供权限,如下所示,您可以看到我们只授予了该用户对“Insert
”、“Select
”、“Update
”的权限。
拥有最低权限,将有助于我们保护数据库免受攻击。
使用存储过程
存储过程是参数化查询的一种形式。使用存储过程也是一种防止 SQL 注入攻击的方法。
在下面的快照中,我们删除了之前使用的内联查询。之前,我们已经编写了通过存储过程从数据库获取数据的代码,这有助于我们防止 SQL 注入攻击,然后对于存储过程,我们需要传递参数 [@RegistrationID]
,并根据参数,它将从数据库中获取记录,在传递参数后,我们使用了 CommandType [CommadType.StoredProcedure]
,这表明我们正在使用存储过程。
注意:始终将参数与存储过程一起使用,如果您不使用它,您仍然容易受到 SQL 注入攻击。
使用存储过程后,让我们请求演示视图。
显示记录的注册视图
使用存储过程后的 Profiler 视图
显示我们使用的存储过程的跟踪。
使用存储过程,让我们再次尝试 SQL 注入攻击。
使用存储过程后,在调试模式下显示 SQL 注入攻击执行情况
如果您查看参数 id [?Id=1or 1=1],其中包含恶意 SQL 脚本,在将其传递给存储过程后,它会显示一个错误,表明它没有被执行,因为我们传递的参数是整数,只接受数字作为输入,如果我们传递恶意 SQL 脚本 [?Id=1 or 1=1],它会抛出错误。
使用存储过程后的 Profiler 视图
输出
使用参数化查询
使用参数化查询是防止 SQL 注入攻击的另一种解决方案。它与存储过程类似。除了连接字符串,您需要将参数添加到 SQL 查询中,并使用 SqlCommand
传递参数值。
参数化查询的 Profiler 视图
输出
使用 ORM(实体框架)
ORM 代表对象关系映射器,它将 SQL 对象映射到您的域对象 [C#]。
如果您正确使用实体框架,您就不容易受到 SQL 注入攻击,因为实体框架内部使用参数化查询。
实体框架的正常执行
在这里,我们已将姓名作为参数 [?name=Saineshwar]
传递。
执行期间的控制器快照
在调试模式下,我们可以看到我们从 Querystring 传递了 name 参数,然后将其传递给 LINQ 查询以获取记录。
LINQ 查询的 Profiler 视图
实体框架内部使用参数化查询,这是真的,这里有一个快照。
尝试对实体框架进行 SQL 注入攻击
在这部分,我们正在尝试对实体框架进行 SQL 注入攻击。为此,我们传递了参数 [?firstName=Saineshwar or 1=1]。
执行期间的控制器快照
在调试模式下,我们可以看到我们从 Querystring
传递了 name 参数,然后将其传递给 LINQ 查询以获取记录,但这次,它还包含一个恶意脚本以及正常的参数。
LINQ 查询的 Profiler 视图
如果仔细查看跟踪,它将名称和恶意脚本视为一个参数,从而阻止了 SQL 注入攻击。
结论
在本文中,我们已按照步骤学习了如何保护 ASP.NET Core MVC 应用程序免受 OWSAP(开放 Web 应用程序安全项目)列出的十大攻击。
感谢您的阅读。希望您喜欢我的文章。
历史
- 2018 年 9 月 5 日:初始版本