10 个要点,确保您的 ASP.NET MVC 应用程序安全






4.91/5 (122投票s)
在本文中,我们将介绍 10 个要点,帮助我们使 MVC 代码更安全。
引言
许多 ASP.NET MVC 开发人员在交付、编写高性能代码等方面都很出色。但在安全性方面,却没有任何计划。因此,在本文中,我们将介绍 10 个要点,帮助我们使 MVC 代码更安全。
如果您是 MVC 新手,我建议您从这个 YouTube 教程开始。
目录
- 安全配置错误(错误处理必须设置自定义错误页面)
- 跨站请求伪造 (CSRF)
- 跨站脚本 (XSS) 攻击
- 恶意文件上传。
- 版本泄露
- SQL 注入攻击
- 敏感数据泄露
- 审计跟踪
- 身份验证和会话管理中断
- 未经验证的重定向和转发
1) 安全配置错误(错误处理必须设置自定义错误页面)
在此类攻击中,攻击者会截获最终用户提交的表单数据,更改值,然后将修改后的数据发送到服务器。
因此,对于这类场景,开发人员会进行适当的验证,但当这些验证显示错误时,服务器的许多信息都会泄露。
因此,让我们实际演示一下。
示例
为了演示,我创建了一个 Employee
表单,该表单会收集基本的 Employee
详细信息。
EmployeeDetailModel 视图
那么,数据注解验证是否足以保护页面安全?不,这不足以保护页面。我将向您展示一个小型演示,说明如何绕过这些验证。
如果您是数据注解验证的新手,请观看此 YouTube 视频,其中解释了如何使用数据注解进行验证。
下面的快照显示地址字段正在验证。它只要求 50 个字符。
拦截“添加员工”视图
现在,让我们拦截此表单,然后从拦截处将其提交到服务器。我正在使用一个名为 burp suit 的工具,它可以捕获您发送到服务器和来自服务器的请求。
在下面的快照中,我捕获了一个发送到服务器的请求。
在下面的快照中,我捕获的请求正在发送到服务器。您可以看到我更改了地址,地址只接受 50 个字符。我添加了超过 50 个字符,然后将其提交到服务器。
拦截“添加员工”表单的地址字段
下面的快照显示了已提交到服务器的请求,该请求包含超过 50 个字符。
“员工”表单的调试模式
在向 Address
字段提交超过 50 个字符后,服务器会抛出异常。因为在数据库中,Address
字段的数据类型是 varchar(50)
,而数据超过 50 个字符,所以出现异常是理所当然的。
向用户显示错误的问题
现在,发生的异常直接显示给攻击者,这会泄露大量关于服务器和我们程序行为的宝贵信息。利用这些错误信息,他可以尝试各种组合来利用我们的系统。
解决方案
因此,这里的解决方案是我们需要设置某种错误页面,该页面不显示内部技术错误,而是显示自定义错误消息。
我们有两种方法可以做到:
- 创建自定义错误处理属性。
- 从 Web.config 文件设置自定义错误页面。
解决方案 1
使用 HandleErrorAttribute
或 IExceptionFilterFilter
创建自定义错误处理属性。
使用 HandleErrorAttribute
显示示例。
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Web.Mvc;
namespaceMvcSecurity.Filters
{
publicclassCustomErrorHandler : HandleErrorAttribute
{
publicoverridevoidOnException(ExceptionContextfilterContext)
{
Exception e = filterContext.Exception;
filterContext.ExceptionHandled = true;
var result = newViewResult()
{
ViewName = "Error"
}; ;
result.ViewBag.Error =
"Error Occur While Processing Your Request Please Check After Some Time";
filterContext.Result = result;
}
}
创建自定义 Error
属性后,我们需要将其全局应用于整个应用程序。为此,我们需要在 App_Start 文件夹中的 FilterConfig
类中调用此属性,如下所示:
usingMvcSecurity.Filters;
usingSystem.Web;
usingSystem.Web.Mvc;
namespaceMvcSecurity
{
publicclassFilterConfig
{
publicstaticvoidRegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(newCustomErrorHandler());
}
}
}
每当发生错误时,CustomErrorHandler
属性就会被调用,它将重定向到 Error.cshtml 页面。如果您想传递任何消息,可以通过 @ViewBag.Errorfrom CustomErrorHandler
属性传递。
HTML 错误页面代码片段
@{
Layout = null;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Error</title>
</head>
<body>
<hgroup>Error.
<h3>@ViewBag.Error</h3>
<h3> </h3>
</hgroup>
</body> </html>
错误页面视图
解决方案 2
- 从 Web.config 文件设置自定义错误页面。
- 如果您不想编写属性,那么可以在 Web.config 文件中设置自定义错误页面。在此之前,只需创建一个简单的 HTML 错误页面,以便在发生任何错误时显示。
在 Web.config 文件中,有一个 system.web 标签。在其中,添加 Custom error 标签,如下所示:
HTML 错误页面代码片段
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Error</title>
</head>
<body>
<hgroup>Error.
<h2>An Error Occurred While Processing Your request...........</h2>
<h2> </h2>
</hgroup>
</body>
</html>
HTML 错误页面视图
哇,我们刚刚开始保护我们的 Web 应用程序。让我们来看第二点。
2) 跨站请求伪造 (CSRF)
CSRF 漏洞允许攻击者在未经用户同意或用户不知情的情况下,强迫已验证并登录的用户执行操作。
举个简单的例子
- 用户登录银行服务器。
- 银行授权并建立了用户与银行服务器之间的安全会话。
- 攻击者向用户发送一封带有恶意链接的电子邮件,上面写着“立即赚取 100000 美元”。
- 用户点击恶意链接,网站尝试从您的账户向攻击者账户转账。由于安全会话已建立,恶意代码就可以成功执行。
微软已经认识到这种威胁,并为此提供了一种名为 AntiForgeryToken 的机制。
解决方案
我们需要在表单的表单标签内添加 @Html.AntiForgeryToken()
助手。在处理您的 post([HttpPost]
)请求的操作方法上,我们需要放置 [ValidateAntiForgeryToken]
属性,该属性将检查令牌是否有效。
向视图添加 [AntiForgeryToken] 助手
向 HttpPost 方法添加 [ValidateAntiForgeryToken]
属性。
AntiForgeryToken 的工作原理
当我们向视图添加 AntiForgeryToken
助手时,它会创建一个隐藏字段并为其分配一个唯一的令牌值,同时会将一个会话 Cookie 添加到浏览器。
当我们提交表单 HTML 时,它会检查 __RequestVerificationToken
隐藏字段以及 __RequestVerificationToken
Cookie 是否存在。如果缺少 cookie 或表单 __RequestVerificationToken
隐藏字段的值,或者值不匹配,ASP.NET MVC 将不会处理该操作。这就是我们如何防止 ASP.NET MVC 中的跨站请求伪造攻击。
视图中的 RequestVerificationToken 快照
RequestVerificationToken Cookie 快照
3) 跨站脚本 (XSS) 攻击
跨站脚本 (XSS) 是一种通过输入字段注入恶意脚本的攻击。这种攻击最为常见,允许攻击者窃取凭据和有价值的数据,从而可能导致严重的安全漏洞。
在这种攻击中,攻击者访问一个网站,并尝试在评论框中执行恶意脚本。现在,如果网站没有检查恶意代码,那么代码就可以在服务器上执行,造成损坏。
让我们通过一个例子来理解这一点。下面是一个简单的 Employee
表单,我们试图保存数据。现在,在文本框中,我尝试使用 SCRIPT
标签使用 JavaScript 执行一些恶意代码。但是,如果我们尝试提交,MVC 会抛出错误,提示“发生了一些不好的事情”。
简而言之,默认情况下,ASP.NET 会阻止跨站脚本攻击。
理解显示的错误
客户端检测到潜在的危险 Request.Form
值 (worktype="<script>alert('hi');")
。
发生此错误是因为 MVC 会验证用户输入的数据,如果用户尝试执行此类脚本,则不允许执行,这是个好消息。
但是,如果我们想放置 SCRIPT
标签呢?例如,像 CodeProject 这样的编程网站,最终用户确实需要提交代码和脚本片段。在这些情况下,我们希望最终用户通过 UI 发布代码。
因此,让我们了解如何做到这一点,同时又不损害安全性。
因此,我们有四种方法可以允许脚本被发布。
解决方案
[ValidateInput(false)]
[AllowHtml]
[RegularExpressionAttribute]
AntiXSS
库
解决方案 1
ValidateInput
[ValidateInput]
是一个可以应用于 Controller 或 Action Method 的属性,我们希望脚本能够通过它。
如果我们希望允许标记,则需要将 enableValidation
属性设置为 False ([ValidateInput(false)])
,这将不会验证输入;如果设置为 True ([ValidateInput(true)])
,则会验证输入。同样,如果您将其应用于 Controller,则它适用于 Controller 中的所有 Action 方法;如果您将其应用于 Action Method,则它仅适用于该 Action Method。
但是 ValidateInput 属性将应用于 Model(EmployeeDetails)的所有属性。
在 HttpPostMethod 上应用 ValidateInputAttribute
的快照。
应用 ValidateInputAttribute 后的快照。
解决方案 2
AllowHtml
[AllowHtml]
属性应用于 Model
属性,这样它就不会验证添加了 AllowHtml
属性的特定 Model
属性。这允许提交 HTML 以避免跨站脚本攻击。
在下面的快照中,我将 AllowHtml
属性应用于 EmployeeDetails
模型中的 Address
属性。
在将 AllowHtml
属性应用于该 Address
属性后,现在 Address
属性将不会被验证,并允许在此字段中提交 HTML。
解决方案 3
正则表达式
XSS 攻击的第三种解决方案是使用正则表达式验证所有字段,以便只有有效数据才能通过。
使用正则表达式验证输入以防止 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})?))$
解决方案 4
AntiXSS 库
XSS 攻击的第四种解决方案是使用 MicrosoftAntiXSSLibrary
,它将有助于保护您的应用程序。
现在,让我们开始从 NuGet 安装 MicrosoftAntiXSSLibrary
。只需右键单击 **项目**,然后选择 **管理 NuGet 程序包**。
选择后,将弹出一个名为 ManageNuGetPackages
的新对话框。在此对话框中,在搜索框中搜索 AntiXSSLibrary
,然后选择第一个选项 AntiXSS
,然后单击 **安装** 按钮。
安装后的引用
安装后,我们将研究如何使用 AntiXSSLibrary
。
Sanitizer 类
下面的快照显示了如何使用 Sanitizer 类方法。
Sanitizer
是一个 static
类,可以随处访问。我们只需要将需要验证的输入字段提供给 Sanitizer
类方法(GetSafeHtmlFragment
)。它将进行检查并返回 Sanitize string
。
我们可以使用此方法在将数据保存到数据库和显示在浏览器中时过滤恶意脚本。
提示:在使用此 AntiXSS
库之前,请使用 [ValidateInput(false)]
或 [AllowHtml]
,否则会抛出“A potentially dangerous Request.Form
”错误。
4) 恶意文件上传
到目前为止,我们已经学习了如何保护所有输入字段免受攻击,但仍然缺少一个主要字段,那就是文件上传控件。我们需要保护其免受无效输入的影响,大多数攻击者会尝试上传恶意文件,这可能导致安全问题。攻击者可以更改文件扩展名 [tuto.exe 到 tuto.jpeg],恶意脚本可能会作为图像文件上传。大多数开发人员只查看文件的文件扩展名并将其保存在文件夹或数据库中,但文件扩展名可能是有效的,但文件本身可能包含恶意脚本。
解决方案
- 我们需要做的第一件事是验证文件上传。
- 仅允许访问所需的文件扩展名。
- 检查文件头。
首先,我要在 View 中添加一个文件上传控件。
添加文件上传控件
我们在视图中添加了文件上传控件。接下来,我们将在提交时验证文件。
在 Index HttpPost 方法中验证文件上传
在此方法中,我们首先验证文件的 Content-Length
。如果为零 [upload.ContentLength == 0
],则用户未上传文件。
如果 Content-Length
大于零,则表示有文件 [upload.ContentLength> 0]
,我们将读取文件的 Filename、Content Type 和 File Bytes。
[HttpPost]
[ValidateAntiForgeryToken]
publicActionResult Index(EmployeeDetailEmployeeDetail)
{
if (ModelState.IsValid)
{
HttpPostedFileBase upload = Request.Files["upload"];
if (upload.ContentLength == 0)
{
ModelState.AddModelError("File", "Please Upload your file");
}
elseif (upload.ContentLength> 0)
{
stringfileName = upload.FileName; // getting File Name
stringfileContentType = upload.ContentType; // getting ContentType
byte[]tempFileBytes= newbyte[upload.ContentLength]; // getting filebytes
var data = upload.InputStream.Read(tempFileBytes, 0, Convert.ToInt32(upload.ContentLength));
var types = MvcSecurity.Filters.FileUploadCheck.FileType.Image; // Setting Image type
var result = FileUploadCheck.isValidFile
(tempFileBytes, types, fileContentType); // Validate Header
if (result == true)
{
intFileLength = 1024 * 1024 * 2; //FileLength 2 MB
if (upload.ContentLength>FileLength)
{
ModelState.AddModelError("File", "Maximum allowed size is: " + FileLength + " MB");
}
else
{
stringdemoAddress = Sanitizer.GetSafeHtmlFragment(EmployeeDetail.Address);
dbcon.EmployeeDetails.Add(EmployeeDetail);
dbcon.SaveChanges();
return View();
}
}
}
}
return View(EmployeeDetail);
}
到目前为止,我们已经完成了基本的验证。现在,让我们验证上传的文件。为此,我编写了一个名为 FileUploadCheckin
的 static
类。此类包含各种用于验证不同文件类型的方法。目前,我将向您展示如何验证图像文件,并仅允许图像文件。
FileUploadCheck 类
在上一个快照中,有一个 ImageFileExtension enum
,其中包含 Image
格式和 File
类型。
privateenumImageFileExtension
{
none = 0,
jpg = 1,
jpeg = 2,
bmp = 3,
gif = 4,
png = 5
}
publicenumFileType
{
Image = 1,
Video = 2,
PDF = 3,
Text = 4,
DOC = 5,
DOCX = 6,
PPT = 7,
}
如果通过了基本验证,我们将调用 isValidFile
方法,该方法以字节、文件类型和 FileContentType
作为输入。
publicstaticboolisValidFile(byte[] bytFile, FileTypeflType, StringFileContentType)
{
boolisvalid = false;
if (flType == FileType.Image)
{
isvalid = isValidImageFile(bytFile, FileContentType);//we are going call this method
}
elseif (flType == FileType.Video)
{
isvalid = isValidVideoFile(bytFile, FileContentType);
}
elseif (flType == FileType.PDF)
{
isvalid = isValidPDFFile(bytFile, FileContentType);
}
returnisvalid;
}
调用 isValidFilemethod
后,它将调用另一个基于文件类型的 static
方法。
如果文件类型是图像,则调用第一个方法 [isValidImageFile]
;如果文件类型是 Video
,则调用第二个方法 [isValidVideoFile]
;依此类推,如果文件类型是 PDF,则调用最后一个方法 [isValidPDFFile]
。
在完成对 isValidFileMethod
的理解后,让我们看一下将要调用的 [isValidImageFile]
方法。
下面是 [isValidImageFile]
方法的完整代码片段。
在此方法中,我们允许有限的图像文件扩展名 [jpg, jpeg, png, bmp, gif]。
此 isValidImageFile 方法的工作原理
当我们将字节和 FileContentType
传递给此方法时,它将首先根据 FileContentType
进行检查,然后据此进行设置。
ImageFileExtension
之后,它将检查我们拥有的头字节是否与上传文件的字节匹配。如果匹配,则文件有效 [true
],否则文件无效 [false
]。
publicstaticboolisValidImageFile(byte[] bytFile, StringFileContentType)
{
boolisvalid = false;
byte[] chkBytejpg = { 255, 216, 255, 224 };
byte[] chkBytebmp = { 66, 77 };
byte[] chkBytegif = { 71, 73, 70, 56 };
byte[] chkBytepng = { 137, 80, 78, 71 };
ImageFileExtensionimgfileExtn = ImageFileExtension.none;
if (FileContentType.Contains("jpg") | FileContentType.Contains("jpeg"))
{
imgfileExtn = ImageFileExtension.jpg;
}
elseif (FileContentType.Contains("png"))
{
imgfileExtn = ImageFileExtension.png;
}
elseif (FileContentType.Contains("bmp"))
{
imgfileExtn = ImageFileExtension.bmp;
}
elseif (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;
}
}
}
}
}
returnisvalid;
}
从 Action Method 调用 isValidFile 方法
我们将调用(FileUploadCheck.isValidFile
)方法,然后传递参数 File Bytes、Types、FileContentType
。
此方法将返回布尔值;如果文件有效,则返回 true
,否则返回 false
。
stringfileName = upload.FileName; // getting File Name
stringfileContentType = upload.ContentType; // getting ContentType
byte[] tempFileBytes = newbyte[upload.ContentLength]; // getting filebytes
var data = upload.InputStream.Read(tempFileBytes, 0, Convert.ToInt32(upload.ContentLength));
var types = MvcSecurity.Filters.FileUploadCheck.FileType.Image; // Setting Image type
var result = FileUploadCheck.isValidFile
(tempFileBytes, types, fileContentType); //Calling isValidFile method
在理解了代码片段后,现在让我们通过一个演示看看它是如何工作的。
下面的快照显示了一个带有文件上传控件的“员工”表单。
我们将填写下面的表单并选择一个有效文件。
选择一个有效的 .jpg 文件,并详细检查它将如何验证。
从磁盘中选择一个 .jpg 图像。
选择文件后的“员工”表单快照。
在此部分,我们已选择了一个文件。
Index Post Action Method 的调试快照。
在此部分,我们提交了一个带有文件的 Employee
表单。在这里,我们可以看到它如何进行基本验证。
Index Post Action Method 的调试快照。
在此部分,您可以看到我们上传的文件的实时值。它已通过基本验证。
调用 isValidFile 方法的 FileUploadCheck Class 快照。
在此部分,调用 isValidFile
方法后,它将根据其 FileContentType
调用另一个方法。
检查 isValidImageFile 方法以检查头字节。
在此方法中,它将检查上传的图像的头字节是否与我们拥有的字节匹配。如果匹配,则文件有效,否则无效。
5) 版本泄露
版本信息可供攻击者利用,针对特定版本进行攻击。
每当浏览器向服务器发送 HTTP 请求时,我们都会收到响应头,其中包含 [Server, X-AspNet-Version, X-AspNetMvc-Version, X-Powered-By]
的信息。
服务器显示有关正在使用的 Web 服务器的信息。
X-AspNet-Version
显示有关使用的特定 ASP.NET 版本的信息。X-AspNetMvc-Version
显示有关使用的 ASP.NET MVC 版本的信息。X-Powered-By
显示有关您的网站运行在哪种框架上的信息。
解决方案
- 删除 X-AspNetMvc-Version 标头
要删除显示 ASP.NET MVC 版本信息的响应
X-AspNetMvc-Version
,MVC 中有一个内置属性。只需在 Global.asax 的 Application start 事件
[Application_Start()]
中设置[MvcHandler.DisableMvcResponseHeader = true;]
即可删除标头。它将不再显示。图 40. 在 Global.asax 中设置属性以从标头中删除 X-AspNetMvc-Version。图 41. 从标头中删除 X-AspNetMvc-Version 后的响应。 - 删除 X-AspNet-Version 和 Server 标头
要删除显示所使用 Web 服务器信息的 Server 标头响应,以及
X-AspNet-Version
标头显示所使用特定 ASP.NET 版本的信息。只需在 global.asax 的
[Application_PreSendRequestHeaders()]
中添加一个事件,然后要删除标头,我们需要如下设置属性:protectedvoidApplication_PreSendRequestHeaders() { Response.Headers.Remove("Server"); //Remove Server Header Response.Headers.Remove("X-AspNet-Version"); //Remove X-AspNet-Version Header }
图 42. 在 global.asax 中添加 Application_PreSendRequestHeaders 事件,然后删除响应头。图 43. 从标头中删除 X-AspNet-Version 和 Server 后的响应。 - 删除 X-Powered-By 标头
要删除显示您的网站运行在哪种框架上的信息的响应
X-Powered-By
标头。只需在 Web.config 文件中的
System.webServer
下添加此标记即可删除[X-Powered-By]
标头。<httpprotocol> <customheaders> </customheaders> </httpprotocol>
图 44. 在 Web.config 中添加自定义 Header 标记以删除响应头。图 45. 从标头中删除 X-Powered-By 后的响应。
6) SQL 注入攻击
SQL 注入攻击是最危险的攻击之一。它在 OWASP2013 [Open Web Application Security Project] 的十大漏洞中排名第一。SQL 注入攻击可以为攻击者提供有价值的数据,从而导致严重的安全性漏洞,甚至可以完全访问数据库服务器。
在 SQL 注入中,攻击者总是尝试输入恶意 SQL 语句,这些语句将在数据库中执行并返回不必要的数据给攻击者。
显示用户数据的简单视图
视图根据下面的快照所示的 EmployeeID
显示单个 Employee
数据。
SQL 注入攻击后显示所有用户数据的简单视图
在此浏览器视图中,攻击者看到应用程序 URL 包含一些有价值的数据,即 **ID [https://:3837/EmployeeList/index?Id=2]**,攻击者尝试 SQL 注入攻击,如下所示:
在尝试了 SQL 注入攻击的排列组合后,攻击者获得了所有用户数据的访问权限。
在调试模式下显示 SQL 注入
在这里,我们可以详细了解攻击者如何传入恶意 SQL 语句并在数据库中执行。
SQL Profiler 视图的 SQL 语句
解决方案
- 验证输入
- 使用低权限的数据库登录
- 使用参数化查询
- 使用 ORM(例如 Dapper、Entity Framework)
- 使用存储过程
1. 验证输入
始终在客户端和服务器端都验证输入,以确保输入中不包含允许攻击者进入系统的特殊字符。
在 MVC 中,我们使用数据注解进行验证。
数据注解属性是应用于模型以验证模型数据的简单规则。
客户端验证输入
服务器端验证输入
Model state 为 false
- 这表明模型无效。
2. 授予最低权限的数据库登录
Db_owner
是数据库的默认角色,可以授予和撤销访问权限、创建表、存储过程、视图、运行备份、计划作业,甚至可以删除数据库。如果将此类角色访问权限授予数据库,那么正在使用的用户将拥有完全访问权限,并且可以对其执行各种活动。我们必须创建一个具有最低权限的新用户账户,并仅授予该用户必须访问的权限。
例如,如果用户需要处理与选择、插入和更新 Employee
详细信息相关的操作,那么应该只提供 select
、Insert
和 update
权限。
添加新用户并授予权限的步骤
在此部分,我展示了一个如何创建用户并授予特定权限的小示例。
- 创建新用户
- 创建用户后的视图
- 选择用户可以访问的对象(表)
- 选择要授予权限的特定表
选择表后,我们将向表授予权限,如下所示。您可以看到我们仅为此用户授予了“Insert
”、“Select
”、“Update
”权限。
最低权限有助于我们保护数据库免受攻击。
3. 使用存储过程
存储过程是一种参数化查询。使用存储过程也是一种防止 SQL 注入攻击的方法。
在下面的快照中,我们删除了之前使用的内联查询,现在我们编写了使用存储过程从数据库获取数据的代码,这有助于我们防止 SQL 注入攻击。然后,我们需要将参数 [@EmpID]
传递给存储过程,并根据参数获取数据库记录。传递参数后,我们使用了 CommandType
[CommandType.StoredProcedure]
,这表示我们正在使用存储过程。
注意:始终对存储过程使用参数;如果您不使用,仍然容易受到 SQL 注入攻击。
使用存储过程后,我们请求 GetEmployee 视图
显示记录的员工视图。
使用存储过程后的 Profiler 视图
显示我们使用的存储过程的跟踪。
使用存储过程后,再次尝试 SQL 注入攻击
使用存储过程后,在调试模式下显示 SQL 注入攻击的执行情况
如果您查看参数 id [?Id=2 或 1=1],它包含恶意 SQL 语句。在将其传递给存储过程后,它会显示一个错误,表明它没有被执行,因为传递的参数是整数,只接受数字作为输入。如果我们传递恶意 SQL 语句 [?Id=2 或 1=1],它会抛出错误。
使用存储过程后的 Profiler 视图
输出
同时,您可能会认为,如果它是 varchar
数据类型,那么我们已经成功攻击了。让我们也看看演示。
使用不同参数 @name 的存储过程后,在调试模式下显示 SQL 注入攻击的执行情况
这是第二个演示,我们在其中传递了不同的参数给存储过程,以查看它是否真的能防止 SQL 注入攻击。
输出
使用参数化查询
使用参数化查询是防止 SQL 注入攻击的另一种解决方案。它与存储过程类似。您需要将参数添加到 SQLQuery 中,并使用 SqlCommand
传递参数值,而不是连接字符串。
参数化查询的 Profiler 视图
输出
使用 ORM(Entity Framework)
ORM 代表对象关系映射器,它将 SQL 对象映射到您的域对象 [C#]。
如果正确使用 Entity Framework,您就不会容易受到 SQL 注入攻击,因为 Entity Framework 内部使用了参数化查询。
Entity Framework 的正常执行
在这里,我们已将名称作为参数传递 [?name=Saineshwar
]。
控制器执行期间的快照
在调试模式下,我们可以看到我们已经从 Querystring
传递了 name 参数,然后将其传递给 Linq 查询以获取记录。
Linq 查询的 Profiler 视图
Entity Framework 内部使用参数化查询,这是真的,这里是快照。
让我们尝试对 Entity Framework 进行 SQL 注入攻击
在此部分,我们正在尝试对 Entity Framework 进行 SQL 注入攻击。为此,我们已将参数传递为 [?name=Saineshwar or 1=1]
。
控制器执行期间的快照
在调试模式下,我们可以看到我们已经从 Querystring
传递了 name 参数,然后将其传递给 Linq 查询以获取记录,但这次,它除了普通参数外,还包含恶意脚本。
Linq 查询的 Profiler 视图
如果仔细查看跟踪,它会将 name 和恶意脚本视为单个参数,这可以防止 SQL 注入攻击。
7) 敏感数据泄露
所有网站和应用程序都有数据库,其中存储着所有数据。这意味着当我们存储用户的个人信息(可能包含密码、PAN 号、护照详细信息、信用卡号)时,我们通常只加密密码,而其他数据则以明文形式存储,这可能导致敏感数据泄露。当攻击者攻击时,他可以访问数据库,如果他找到存储所有这些个人和财务详细信息的表,就可以窃取这些信息。
一个关于敏感数据如何被窃取的简单演示。
登录页面快照
创建项目时选择 Internet 模板时默认的登录页面简单代码片段。
在查看了登录页面的标记和视图后,接下来我们将输入凭据并登录。
输入登录凭据
在登录页面,我们将输入用户名和密码以登录应用程序。
攻击者拦截登录页面以窃取凭据
当用户在登录页面输入凭据并提交到服务器时,数据(用户名和密码)会以明文形式传输到服务器。这些数据(用户名和密码)可以被攻击者拦截并窃取您的凭据。
下面的快照显示了您的值如何被攻击者拦截。
解决方案
- 始终以加密格式通过强哈希和种子(随机哈希)将敏感数据发送到服务器。
- 始终为 Web 应用程序应用 SSL。
- 如果要在数据库中存储敏感数据,请不要以明文形式存储,而是使用强哈希技术。
始终以加密格式通过强哈希和种子将敏感数据发送到服务器。
在此演示中,我们将使用 MD5 算法与种子在客户端加密数据,然后发送到服务器,以便攻击者无法窃取。
在下面的快照中,用户输入凭据。
在用户输入凭据后,当用户点击登录按钮时,用户输入的密码将与客户端上的种子一起使用 MD5 加密,然后发送到服务器。在此过程中,如果攻击者尝试嗅探网络,则只能看到加密哈希,无法解密。
详细来说,您可以看到下面的快照,攻击者已经嗅探了网络。
快照中的蓝色线条表示 **用户名**。
快照中的红色线条表示 **加密的密码(连同种子)**。
快照中的绿色线条表示 **从服务器生成的种子值**。
到目前为止,我们已经看到了如何做到这一点的快照。让我们来看一个代码片段。
登录模型
下面是 [HttpGet]
Login ActionMethod
的代码片段。
在此方法中,我们将生成随机哈希 [Seed]
,然后将其分配给 LoginModel
,然后将其传递给 Login 视图。
在 Login ActionMethod
中将 [hdrandomSeed]
的值分配给模型后,现在我们将在 Login 视图中使用它作为一个隐藏字段。
现在,在页面上添加了一个隐藏字段后,让我们看看如何使用 MD5 JavaScript 和种子加密数据。
为了做到这一点,我们将使用 jquery 库 1.8 和 md5.js 库。当用户输入凭据并点击“登录”按钮时,我们将获取用户输入的密码 [var password1 = $('#Password');]
并首先生成其哈希 [calcMD5(password).toUpperCase()]
,然后与种子一起,我们再次生成哈希 [var hash = calcMD5(seed + calcMD5(password).toUpperCase());]
,它是唯一的,然后将其发送到服务器。
请看下面的快照了解详情。
登录页面的调试视图
在会话变量中,您可以看到用户点击登录按钮时生成的实时值。
在客户端加密后,现在让我们看看攻击者是否能够拦截并看到明文密码。
拦截登录页面
在下面的快照中,您可以看到用户输入的密码是加密格式的,攻击者无法理解,因为它是由用户输入的种子 + 密码的组合哈希。
拦截后,接下来我们将查看值如何发布到 Login Action 方法。
从 Login 视图发布值后的 Login Action 方法
在这里,我们可以清楚地看到密码是加密格式的。
在下一步中,我们将比较存储在数据库中的密码。为此,我们首先将从用户名获取密码(用户已输入),然后将其与数据库中存储的密码进行比较。用绿色标记的行是种子值。此外,您可以看到用红色标记的数据库存储密码(通过用户名获取),黄色线条表示此密码是从 Login 视图发布的,最后,蓝色线条是数据库密码和种子的组合。我们将其与发布的密码进行比较。
最终,用户输入的敏感数据是安全的。
2. 使用 SSL 保护 Web 应用程序
SSL(安全套接字层)是一个层,它保护客户端和服务器之间的通信(加密),以便从客户端和服务器传递的任何数据(银行详细信息、密码和其他金融交易)都是安全的(加密的)。
SSL 主要应用于登录页面和支付网关;如果您想将其应用于整个应用程序,也可以。
如果您想详细了解如何在 IIS 服务器上启用 SSL,请参考 Scott Guthrie 先生博客上的这篇好文章。
3. 不要以明文形式将敏感数据存储在数据库中
切勿将信用卡、借记卡、财务详细信息和其他敏感详细信息以明文形式存储在数据库中。始终使用强哈希技术加密数据,然后存储在数据库中。如果攻击者直接访问数据库,那么所有明文格式的数据都可能被泄露。
以下是可根据需要使用的算法列表。
哈希算法
如果有人只需要哈希,他们可以使用哈希算法。我们主要使用哈希函数来加密密码。
SymmetricAlgorithm
如果有人只需要一个密钥进行加密和解密,他们可以使用 SymmetricAlgorithm
。
AsymmetricAlgorithm
如果有人只需要一个密钥进行加密(公钥)和另一个密钥进行解密(私钥),他们可以使用 AsymmetricAlgorithm
。例如,当我们将 Web 服务和 WebAPI 与客户端(用户)共享时,我们可以使用它。
HashAlgorithm
- MD5
- SHA256
- SHA384
- SHA512
示例
生成 MD5 哈希的方法
privatestring Generate_MD5_Hash(stringdata_To_Encrypted)
{
using (MD5encryptor = MD5.Create())
{
MD5md5 = System.Security.Cryptography.MD5.Create();
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(data_To_Encrypted);
byte[] hash = md5.ComputeHash(inputBytes);
StringBuildersb = newStringBuilder();
for (int i = 0; i < hash.length; i="">
传递文本以生成哈希
string Hash = Generate_MD5_Hash("Hello");
输出
SymmetricAlgorithm
- Aes
- DES
- RC2
- Rijndael
- TripleDES
示例
AES 加密的 AES 方法
privatestringEncrypt_AES(stringclearText)
{
stringEncryptionKey = "##SAI##1990##"; //Encryption Key
byte[] clearBytes = Encoding.Unicode.GetBytes(clearText);
byte[] array = Encoding.ASCII.GetBytes("##100SAINESHWAR99##"); //salt
using (Aesencryptor = Aes.Create())
{
Rfc2898DeriveBytespdb = newRfc2898DeriveBytes(EncryptionKey, array);
encryptor.Key = pdb.GetBytes(32);
encryptor.IV = pdb.GetBytes(16);
using (MemoryStreamms = newMemoryStream())
{
using (CryptoStreamcs =
newCryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(clearBytes, 0, clearBytes.Length);
cs.Close();
}
clearText = Convert.ToBase64String(ms.ToArray());
}
}
returnclearText;
}
AES 解密的 AES 方法
privatestringDecrypt_AES(stringcipherText)
{
stringEncryptionKey = "##SAI##1990##"; //Encryption Key
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] array = Encoding.ASCII.GetBytes("##100SAINESHWAR99##"); //salt
using (Aesencryptor = Aes.Create())
{
Rfc2898DeriveBytespdb = newRfc2898DeriveBytes(EncryptionKey, array);
encryptor.Key = pdb.GetBytes(32);
encryptor.IV = pdb.GetBytes(16);
using (MemoryStreamms = newMemoryStream())
{
using (CryptoStreamcs =
newCryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(cipherBytes, 0, cipherBytes.Length);
cs.Close();
}
cipherText = Encoding.Unicode.GetString(ms.ToArray());
}
}
returncipherText;
}
要加密的值的文本
stringDataEncrypt = Encrypt_AES("Hello"); // Encrypting Data (Pass text to Encrypt)
要解密的值的文本
stringDataDecrypt = Decrypt_AES(DataEncrypt); // Decrypt data (Pass Encrypt text to Decrypt)
输出
AsymmetricAlgorithm
- DSA
- ECDiffieHellman
- ECDsa
- RSA
示例
RSA 加密的 RSA 方法
publicbyte[] Encrypt(stringpublicKeyXML, stringdataToDycript)
{
RSACryptoServiceProviderrsa = newRSACryptoServiceProvider();
rsa.FromXmlString(publicKeyXML);
returnrsa.Encrypt(ASCIIEncoding.ASCII.GetBytes(dataToDycript), true);
}
RSA 解密的 RSA 方法
publicstring Decrypt(stringpublicPrivateKeyXML, byte[] encryptedData)
{
RSACryptoServiceProviderrsa = newRSACryptoServiceProvider();
rsa.FromXmlString(publicPrivateKeyXML);
returnASCIIEncoding.ASCII.GetString(rsa.Decrypt(encryptedData, true));
}
输出
8) 审计跟踪
在 IT 界,审计跟踪用于跟踪用户在 Web 应用程序上的活动。它对于检测安全问题、性能问题和 ApplicationsLevel
错误问题很重要。它还有助于我们轻松跟踪问题所在并解决它。
解决方案
保留 Web 应用程序上所有用户活动的审计跟踪,并始终监控它。
为了维护审计跟踪,我们首先要在数据库中创建一个表来存储审计数据,表名为 [AuditTB]
。之后,我们将创建一个名为 [UserAuditFilter]
的 ActionFilterAttribute
,在其中,在 Action 执行时,我们将编写代码将访问我们应用程序的用户的数据库数据插入其中。
AuditTB 表视图
在此表中,我们包含了识别用户及其活动所需的常见字段。
显示表视图后,现在让我们看看根据表创建的模型。
AuditTB 模型
publicpartialclassAuditTB
{
publicintUsersAuditID { get; set; }
publicintUserID { get; set; }
publicstringSessionID { get; set; }
publicstringIPAddress { get; set; }
publicstringPageAccessed { get; set; }
publicNullable<system>LoggedInAt { get; set; }
publicNullable<system.datetime>LoggedOutAt { get; set; }
publicstringLoginStatus { get; set; }
publicstringControllerName { get; set; }
publicstringActionName { get; set; }
}
在查看了生成的 Model
后,现在让我们创建一个名为 UserAuditFilter
的 ActionFilter
。
UserAuditFilterActionfilter 代码片段
UserAuditFilter
是我们创建的自定义 ActionFilter
。在此过滤器中,我们将用户活动的数据插入 AuditTB
表。此外,我们还检查用户是否已登录应用程序。同样,我们还插入访问应用程序的用户的 IP 地址,以及用户登录和登出的时间戳。为了插入这些数据,我们使用 ORM Entity Framework。
publicclassUserAuditFilter : ActionFilterAttribute
{
publicoverridevoidOnActionExecuting(ActionExecutingContextfilterContext)
{
AllSampleCodeEntitiesappcontext = newAllSampleCodeEntities();
AuditTBobjaudit = newAuditTB();
//Getting Action Name
stringactionName = filterContext.ActionDescriptor.ActionName;
//Getting Controller Name
stringcontrollerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
var request = filterContext.HttpContext.Request;
if (HttpContext.Current.Session["UserID"] == null) // For Checking User is Logged in or Not
{
objaudit.UserID = 0;
}
else
{
objaudit.UserID = Convert.ToInt32(HttpContext.Current.Session["UserID"]);
}
objaudit.UsersAuditID = 0;
objaudit.SessionID = HttpContext.Current.Session.SessionID; // Application SessionID
// User IPAddress
objaudit.IPAddress =
request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;
objaudit.PageAccessed = request.RawUrl; // URL User Requested
objaudit.LoggedInAt = DateTime.Now; // Time User Logged In ||
// And time User Request Method
if (actionName == "LogOff")
{
objaudit.LoggedOutAt = DateTime.Now; // Time User Logged OUT
}
objaudit.LoginStatus = "A";
objaudit.ControllerName = controllerName; // ControllerName
objaudit.ActionName = actionName; // ActionName
appcontext.AuditTBs.Add(objaudit);
appcontext.SaveChanges(); // Saving in database using Entity Framework
base.OnActionExecuting(filterContext);
}
}
在全局操作过滤器中注册 UserAuditFilter
全局操作过滤器主要用于错误处理和日志记录。
如果您希望将 Action Filter 应用于项目中的所有 Action 方法,那么您可以使用全局操作过滤器。在这里,我们需要全局操作过滤器,因为我们希望跟踪用户的每一次请求以进行审计跟踪,因此我们使用了它。
输出
用户请求页面并进行某些活动时,在 Audit
表中插入的数据。
9) 身份验证和会话管理中断
如果在 Web 应用程序中未正确实现身份验证和会话管理,则可能允许攻击者窃取密码、会话令牌、Cookie,这些问题也可能允许攻击者访问整个应用程序并窃取所有用户凭据。
攻击者窃取数据的方式
- 不安全的连接(未使用 SSL)
- 可预测的登录凭据
- 未以加密形式存储凭据
- 不正确的应用程序注销
可能发生的攻击
1. 会话固定
在找到防止此攻击的解决方案之前,让我们先通过一个小型演示了解会话固定攻击是如何发生的。
每当用户向服务器发送第一个请求时,都会加载登录页面,然后用户输入有效的登录凭据以登录 Web 应用程序。成功登录后,我们在会话中分配一些值以识别唯一用户。同时,一个 [“ASP.NET_SessionId”]
Cookie 会被添加到浏览器以识别发送请求的特定用户,并且 [“ASP.NET_SessionId”]
Cookie 的值将始终发送到服务器,直到您从应用程序注销为止。在注销时,我们基本上编写代码来删除创建的会话值,但我们没有删除登录时创建的 [“ASP.NET_SessionId”]
Cookie。此值有助于攻击者执行会话固定攻击。
会话固定演示
当我们访问登录页面时,浏览器中没有 [“ASP.NET_SessionId”]
Cookie,正如我们在 Cookie 管理器中看到的那样。
用户输入有效凭据后
输入有效的登录凭据 [“ASP.NET_SessionId”]
后,Cookie 会被添加到浏览器。
注意:当任何数据保存到 Session 时,都会创建 [“ASP.NET_SessionId”]
Cookie 并添加到用户浏览器。
从应用程序注销后 Cookie 仍然存在于浏览器中
从应用程序注销后,[“ASP.NET_SessionId”]
Cookie 仍然存在。
注意:预 Cookie 和后 Cookie 相同,这可能导致会话固定。
让我们进行一些会话固定
注销后未移除的 [“ASP.NET_SessionId”]
Cookie 有助于攻击者进行会话固定。我将打开一个浏览器(Chrome)。在那里,我将输入应用程序的 URL [https://:3837/],我们将在此进行会话固定。
在浏览器中输入 URL 后,现在让我们检查此处是否创建了 [“ASP.NET_SessionId”]
Cookie。哦,我们没有任何 Cookie。
已在 Firefox 浏览器中创建的 Cookie
在此视图中,我展示了用户登录时在 Firefox 浏览器中创建的 [“ASP.NET_SessionId”]
Cookie。
注意:为了管理 Cookie,我在 Chrome 浏览器中安装了 Cookie Manager+ 插件。
在查看了 Firefox 中的 [“ASP.NET_SessionId”]
Cookie 后,现在让我们在 Chrome 浏览器中创建与 Firefox 浏览器中的 Cookie 相似的 [“ASP.NET_SessionId”]
Cookie,具有相同的 [“ASP.NET_SessionId”]
Cookie 名称和值。
在 Chrome 浏览器中创建了与 Firefox 浏览器中创建的 Cookie 相似的新 [“ASP.NET_SessionId”] Cookie。
这是我们固定会话的步骤。此会话在另一个浏览器 [Firefox] 上是活动的。我们已复制了类似的值,并创建了一个 [“ASP.NET_SessionId”]
Cookie,并将相同的 SessionID
值分配给此 Cookie。
注意:为了添加 Cookie,我在 Chrome 浏览器中安装了 Edit this Cookie 插件。
固定 Cookie 后,我们不再需要登录应用程序。如果我们只输入应用程序的内部 URL,我们就可以直接访问,因为此会话是在身份验证后创建的。
解决方案
- 注销后移除 [“ASP.NET_SessionId”]
- 保护 Cookie
- 使用 SSL 保护 Cookie 和会话
注销后移除 [“ASP.NET_SessionId”]
注销时,我们会删除 Session 值。同时,我们还会从浏览器中删除 [“ASP.NET_SessionId”]
Cookie。
//
// POST: /Account/LogOff
publicActionResultLogOff()
{
//Removing Session
Session.Abandon();
Session.Clear();
Session.RemoveAll();
//Removing ASP.NET_SessionId Cookie
if (Request.Cookies["ASP.NET_SessionId"] != null)
{
Response.Cookies["ASP.NET_SessionId"].Value = string.Empty;
Response.Cookies["ASP.NET_SessionId"].Expires = DateTime.Now.AddMonths(-10);
}
if (Request.Cookies["AuthenticationToken"] != null)
{
Response.Cookies["AuthenticationToken"].Value = string.Empty;
Response.Cookies["AuthenticationToken"].Expires = DateTime.Now.AddMonths(-10);
}
returnRedirectToAction("Login", "Account");
}
保护 Cookie
为了在登录 [HttpPost]
Action 方法中保护 Cookie,我们将创建新的 Session。在此 Session [Session["AuthenticationToken"]]
中,我们将保存 NewGuid
。同时,我们将添加一个名为 ["AuthenticationToken"]
的 Cookie,它也将具有与 Session 中存储的 [Guid]
相同的值。
代码片段
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
publicActionResult Login(LoginModel model, stringreturnUrl)
{
if (ModelState.IsValid)
{ //Getting Pasword from Database
varstoredpassword = ReturnPassword(model.UserName);
// Comparing Password With Seed
if (ReturnHash(storedpassword, model.hdrandomSeed) == model.Password)
{
Session["Username"] = model.UserName;
Session["UserID"] = 1;
// Getting New Guid
stringguid = Convert.ToString(Guid.NewGuid());
//Storing new Guid in Session
Session["AuthenticationToken"] = guid;
//Adding Cookie in Browser
Response.Cookies.Add(newHttpCookie("AuthenticationToken", guid));
returnRedirectToAction("Index", "Dashboard");
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
return View(model);
}
代码片段描述
创建新的 Guid。
// Getting New Guid
stringguid = Convert.ToString(Guid.NewGuid());
将新的 Guid 保存到 Session。
//Storing new Guid in Session
Session["AuthenticationToken"] = guid;
将新的 Guid 保存到 Cookie 并添加。
//Adding Cookie in Browser
Response.Cookies.Add(newHttpCookie("AuthenticationToken", guid));
存储数据到 Session 并添加 Cookie 到浏览器后,现在让我们在每次请求时匹配这些值并检查它们是否相同。如果不相同,我们将重定向到登录页面。
为了完成这部分,我将在项目中添加一个 AuthorizationFilter
,并在其中编写逻辑来检查 Session 和 Cookie 值是否相同。
AuthenticateUser ActionFilter
如果您查看下面的代码片段,我创建了一个名为 AuthenticateUser
的 AuthorizationFilter
。在此过滤器中,我们继承了 IAuthorizationFilter Interface
和 FilterAttribute
类。通过这些,我们实现了接口中的 [OnAuthorization]
方法,并在该方法中编写了全部逻辑。
using System;
usingSystem.Web.Mvc;
namespaceMvcSecurity.Filters
{
publicclassAuthenticateUser : FilterAttribute, IAuthorizationFilter
{
publicvoidOnAuthorization(AuthorizationContextfilterContext)
{
stringTempSession =
Convert.ToString(filterContext.HttpContext.Session["AuthenticationToken"]);
stringTempAuthCookie =
Convert.ToString(filterContext.HttpContext.Request.Cookies["AuthenticationToken"].Value);
if (TempSession != null&&TempAuthCookie != null)
{
if (!TempSession.Equals(TempAuthCookie))
{
ViewResult result = newViewResult();
result.ViewName = "Login";
filterContext.Result = result;
}
}
else
{
ViewResult result = newViewResult();
result.ViewName = "Login";
filterContext.Result = result;
}
}
}
}
代码片段描述
在此方法中,我们将首先获取 Session 和 Cookie 的值。
stringTempSession = Convert.ToString(filterContext.HttpContext.Session["AuthenticationToken"]);
stringTempAuthCookie = Convert.ToString(filterContext.HttpContext.Request.Cookies
["AuthenticationToken"].Value);
获取 Session 和 Cookie 的值后,现在我们将检查 Session 和 Cookie 的值是否都不为 null
。然后,我们将查看两个值(Session 和 Cookie)是否相等;如果不相等,我们将重定向到登录页面。
if (TempSession != null&&TempAuthCookie != null)
{
if (!TempSession.Equals(TempAuthCookie))
{
ViewResult result = newViewResult();
result.ViewName = "Login";
filterContext.Result = result;
}
}
else
{
ViewResult result = newViewResult();
result.ViewName = "Login";
filterContext.Result = result;
}
在理解了代码片段后,现在我们将将此过滤器应用于用户登录后访问的每个 Controller。
应用 AuthenticateUser 过滤器
将此过滤器应用于用户登录后访问的每个 Controller。
将 Action 过滤器应用于用户登录后访问的所有 Controller 后。
现在,如果攻击者知道 [“ASP.NET_SessionId”]
Cookie 值和一个新的 Cookie [Cookies["AuthenticationToken"]]
值,他仍然无法进行会话固定攻击,因为新的 [Cookies["AuthenticationToken"]]
包含一个唯一的 GUID,并且相同的值存储在 Web 服务器上的 Session [Session["AuthenticationToken"]]
中,但攻击者无法知道存储在 Web 服务器上的 Session 值,并且这些值在用户每次登录应用程序时都会改变,而攻击者用来进行攻击的旧会话值在这种情况下将不再有效。
最后,如果我们允许那些具有有效 Session["AuthenticationToken"]
值和 Cookies["AuthenticationToken"]
值的用户访问应用程序。
两个 Cookie 的实时值。
使用 SSL 保护 Cookie 和会话值
SSL(安全套接字层)是一个保护客户端和服务器之间通信(加密)的层,以便从客户端和服务器传递的任何数据(银行详细信息、密码、会话、Cookie 和其他金融交易)都是安全的(加密的)。
10) 未经验证的重定向和转发
在所有 Web 应用程序中,我们都会从一个页面重定向到另一个页面,有时我们也会重定向到另一个应用程序。但在重定向时,我们不会验证要重定向的 URL,这会导致未经验证的重定向和转发攻击。
这种攻击主要用于网络钓鱼,以获取有价值的详细信息(用户凭据)或向用户的计算机安装恶意软件。
示例
在下面的快照中,您将看到一个简单的 MVC 应用程序 URL,以及攻击者创建的恶意 URL,该 URL 会将用户重定向到一个执行网络钓鱼并向用户计算机安装恶意软件的恶意网站。
原始 URL:https://:7426/Account/Login
攻击者制作的 URL:?returnUrl=https://www.google.co.in
攻击场景
在这种攻击中,用户收到来自攻击者的电子邮件,其中包含与电子商务购物相关的优惠。当用户点击下面的链接时,他会被重定向到购物网站 [http://demotop.com],但如果您仔细查看 URL,您会发现该 URL 包含重定向 [http://demotop.com/Login/Login?url=http://mailicious.com]。现在,在输入有效的用户名和密码后,用户将被重定向到恶意网站 [http://mailicious.com],该网站与 [http://demotop.com] 购物网站类似。在恶意网站上,它将显示消息“用户名或密码无效”,然后用户将再次输入用户名和密码,他将被重定向回原始购物网站,但在此攻击中,用户凭据已被窃取。
解决方案
- 简单地避免使用重定向和转发。
- 如果您仍然想使用重定向和转发,那么请先验证 URL。
- 使用
Url.IsLocalUrl
防止 MVC 中的重定向和转发。
在 MVC 中使用 Url.IsLocalUrl
下面的快照是 MVC 中的登录页面,当用户输入用户名和密码时,它会重定向到 www.google.com。然后他将被重定向到 www.google.com,这是无效的。为了在 MVC4 中防止这种情况,我们有一个内置方法叫做 Url.IsLocalUrl
,它会检查重定向 URL 是否是本地的。如果不是,它将不会重定向。
在理解重定向如何传递之后,现在让我们检查 URL 如何被检查和执行。
当用户输入凭据并提交表单时,下面的 [HttpPost
] Login
方法将被调用。同时,重定向 URL 也会被发布,其中可能包含恶意 URL。为了演示,我只检查了用户名和密码不为 null
。之后,我们将调用 RedirectToLocalAction
方法,并将重定向 URL(returnUrl
)传递给该方法。
在将 URL(returnUrl
)传递给 RedirectToLocalAction
方法后,它将把 URL 传递给 IsLocalUrl
方法 [Url.IsLocalUrl(returnUrl)
],该方法将检查 URL 是否为本地 URL,并返回一个布尔值。如果不是,它将重定向到主页,否则将重定向到传递的 returnUrl
。
[True if the URL is local]
[False if URL is not local]
历史
- 2016 年 8 月 3 日:初始版本