ASP-NET-Core5-安全编程秘籍-全-
ASP.NET Core5 安全编程秘籍(全)
零、序言
ASP.NET Core正迅速成为开发人员选择的 web 应用框架,目前在平台流行度中名列前茅。当 ASP.NET Core web 应用成为恶意攻击的目标时,也不例外。随着越来越多的 web 开发人员编写代码来创建这些 ASP.NET Core web 应用,培训开发人员编写安全代码的需求也在增加。
使用安全代码开发的 ASP.NET Core 应用可以抵御攻击并帮助降低其被利用的风险。有了修复代码中安全缺陷的正确指导,ASP.NET Core web 应用可以防止或停止安全问题。
本书介绍了用 C#编写的代码示例,以及修复通过安全代码检查或安全测试发现的各种 ASP.NET Core web 应用漏洞的步骤。您将找到一些实用的示例和不同的方法来解决配方样式格式的不安全代码引入的安全漏洞。
本书以一章介绍安全编码的基础知识开始,但随后的内容采用 OWASP Top 10(2017 版)模式。OWASP Top 10 实际上是 web 应用最常见风险的标准文档。
本书中的每一章(从第 2 章开始,注入缺陷)代表了每种类型风险的问题解决内容。第 12 章中,杂项漏洞重点关注之前在 OWASP 前 10 名中的各种其他漏洞,以增加覆盖范围。最后一章讨论了安全编码的最佳实践。
在本书结束时,您将能够识别 ASP.NET Core web 应用中不同类型的漏洞,并掌握如何在代码中修复这些漏洞。
这本书是给谁的
本书面向使用 ASP.NET Core 框架开发 web 应用的开发人员和软件工程师。本书是初学者和有经验者的理想之选,它将指导初学者学习编写安全代码的必要基础,并指导经验丰富的人将其作为逐步 ASP.NET Core 安全编码方法的快速来源。
这本书对于希望通过代码保护 ASP.NET Core 应用的详细信息的应用安全工程师来说也是非常好的,它将帮助他们理解如何修复他们每天执行的安全测试发现的问题。
这本书涵盖的内容
第一章安全编码基础是关于每个 ASP.NET Core 开发人员都必须知道的基本安全编码模式。
第 2 章注入缺陷探讨了各种注入缺陷的解决方法,如 SQL 注入、NoSQL 注入、命令注入、LDAP 注入和 XPath 注入。
第 3 章已破坏的身份验证介绍了漏洞的解决方法,这些漏洞主要集中在凭据保护不足、用户枚举、密码要求弱以及会话到期时间不足等方面。
第 4 章敏感数据公开展示了如何在我们的 ASP.NET Core web 应用中实现 HTTPS,启用 HST,确保应用了最新版本的 TLS,并保护加密密钥以防止数据泄漏
第 5 章XML 外部实体介绍了补救恶意 XML 外部实体的方法。本章解释了一个关于 XXE 注入的不安全代码片段示例。将讨论如何修复 XXE 注入的说明。
第 6 章断开的访问控制探索了使用 ASP.NET Core 中内置授权机制的方法,以及实现基于角色的授权以防止未经授权访问 web 应用中的资源的步骤。
第 7 章安全配置错误,讨论了通过关闭代码中的调试、添加安全功能以及通过正确的应用设置阻止不必要的信息泄漏给窥探攻击者来防止安全配置错误的方法。
第 8 章跨站点脚本介绍了修复不同类型 XS 的方法。本章解释反射、存储和 DOM XSS 的不安全代码。本章还解释了如何修复代码中的这些跨站点脚本漏洞。
第 9 章不安全反序列化介绍了如何使用正确配置的库安全地反序列化输入,减轻过时的.NET 类给 ASP.NET Core web 应用带来的风险,并使用更好的反序列化器替代方案来安全地反序列化数据流。
第 10 章使用具有已知漏洞的组件讨论了修复 ASP.NET Core web 应用的方法,这些应用使用具有已知漏洞的组件。
第 11 章、日志记录和监控不足,探讨了解决 ASP.NET Core web 应用中日志记录和监控不足的方法,并解释了这些功能的缺失如何使 web 应用面临风险。
第 12 章杂项漏洞讨论了修复 OWASP 前 10 名列表中不再存在的漏洞的方法以及各种 ASP.NET Core web 应用漏洞。
第 13 章最佳实践介绍了最佳实践配方,这些配方具有经过验证的模式,可实现 ASP.NET Core 安全功能。
充分利用这本书
使用本书的 GitHub 存储库中的示例在线银行 web 应用,按照食谱进行操作。每个配方都有一个before和after文件夹。您将从before文件夹开始使用每个配方,并以样本网上银行 web 应用的初始版本为起点。after文件夹将作为验证所有步骤是否正确执行的参考。

本书中的所有配方都使用 Windows 进行了测试。除了需要仅在 Windows 中运行的软件和插件的配方外,大多数配方也应该在 Linux 和 macOS 中工作(ASP.NET Core 是一个跨平台的 web 框架)。
如果您使用的是本书的数字版本,我们建议您自己键入代码或通过 GitHub 存储库访问代码(下一节提供链接)。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
在 Twitter 上关注作者(可能会让你受益匪浅 https://twitter.com/securecodeninja )或将其添加为 LinkedIn 中的连接(https://www.linkedin.com/in/romancanlas
**## 下载示例代码文件
您可以从 GitHub 的下载本书的示例代码文件 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook 。如果代码有更新,它将在现有 GitHub 存储库中更新。
我们的丰富书籍和视频目录中还有其他代码包,请访问https://github.com/PacktPublishing/ 。看看他们!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:https://static.packt-cdn.com/downloads/9781801071567_ColorImages.pdf 。
使用的约定
本书中使用了许多文本约定。
Code in text:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。下面是一个示例:AddTransient方法允许我们的自定义验证器CustomerValidator被 ASP.NET Core 发现
代码块设置如下:
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
当我们希望提请您注意代码块的特定部分时,相关行或项目以粗体显示:
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
任何命令行输入或输出的编写方式如下:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
-
Bold: Indicates a new term, an important word, or words that you see onscreen. For example, words in menus or dialog boxes appear in the text like this. Here is an example: "Select one record and click its Edit link."
提示或重要提示
看起来像这样。
章节
在这本书中,你会发现几个经常出现的标题(准备就绪、如何做……、如何工作……、还有更多……、另见。
要明确说明如何完成配方,请使用以下部分:
准备好了吗
本节告诉您配方中的预期内容,并介绍如何设置配方所需的任何软件或任何初步设置。
怎么做…
本节包含遵循配方所需的步骤。
它是如何工作的…
本节通常包括对上一节中发生的情况的详细解释。
还有更多…
本节包含有关配方的附加信息,以使您更了解配方。
另见
本节提供了有关配方的其他有用信息的有用链接。
联系
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中注明书名,并发送电子邮件至customercare@packtpub.com。
勘误表:尽管我们已尽一切努力确保内容的准确性,但还是会出现错误。如果您在本书中发现错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的书籍,单击 errata 提交表单链接,然后输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法复制品,请您提供我们的位置地址或网站名称,我们将不胜感激。请致电与我们联系 copyright@packt.com带有指向该材料的链接。
如果您有兴趣成为一名作家:如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请访问authors.packtpub.com。
分享你的想法
一旦您阅读了ASP.NET Core 5 安全编码烹饪书,我们很想听听您的想法!请点击此处,直接进入本书的亚马逊评论页面,并分享您的反馈。
您的评论对我们和技术界都很重要,将帮助我们确保提供高质量的内容。**
一、安全编程基础
理解安全编码原则是成为具有安全意识的 ASP.NET Core 开发人员的基础之一。通过编写安全代码在实践中应用这些概念将有助于您的 web 应用改善其安全状态。
本介绍性章节介绍了每个 ASP.NET Core 开发人员都必须了解的基本安全编码模式。了解这些防御技术将帮助您缓解代码中的安全漏洞,通过这些方法,您将能够了解如何通过使用白名单实现正确的输入验证,执行输入净化,以及如何逃避输出和保护数据。
在本章中,我们将介绍以下配方:
- 使用验证属性启用白名单输入验证
- 使用
FluentValidation库进行白名单验证 - 句法和语义验证
- 输入消毒
- 使用
HTMLSanitizer库输入消毒 - 使用
HtmlEncoder进行输出编码 - 使用
UrlEncoder进行输出编码 - 使用
JavascriptEncoder进行输出编码 - 使用数据保护 API 保护敏感数据
技术要求
本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。这些配方中的代码示例将在 ASP.NET Core Razor 页面中提供。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。您可以在找到本章的代码 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter01 。
输入验证
保护应用免受注入攻击最有效的方法之一是编写正确的输入验证。这种防御性编程技术验证输入是否符合预期的数据格式,例如数据类型、长度或范围(仅举几例)。输入可能来自不受信任的源,在未经验证的情况下,恶意参与者可以向 ASP.NET Core web 应用提供恶意数据,从而可能利用漏洞进行攻击。此过程可能会影响应用,并可能导致应用执行意外操作。
有两种方式验证输入:
- 黑名单
- 白名单
使用黑名单验证策略,在列表中定义已知的错误输入。然后根据该列表验证数据,以决定是否应接受或拒绝输入。然而,这种方法是有缺陷的,因为您只能定义这么多不好的输入,而且它不是一个全面的列表。攻击者只需构造不在黑名单上的有效负载即可绕过此验证。以下是伪代码中的黑名单示例:
when receiving a string input
for each character in string input
if character is in blacklist
reject string input
return error
白名单验证恰恰相反,但它是首选策略。以下是它在伪代码中的等价物:
when receiving a string input
for each character in string input
if character is not in whitelist
reject string input
return error
这里列出了已知良好输入,如果列表中存在数据,则允许数据进入系统,如果不存在,则拒绝。
笔记
本章的大部分内容侧重于输入验证,因为我无法充分强调验证输入的重要性。正确的输入验证可能是最关键的基本安全编码实践,它将使您的 ASP.NET Core web 应用显著提高其应用安全性。
使用验证属性启用白名单验证
Web 开发人员可以利用 ASP.NET Core 提供的内置验证框架。固有数据注释属性(DAA允许您验证绑定到模型属性的值。根据匹配模式进行验证使我们能够过滤输入,并且我们可以将正则表达式指定为白名单。如果模型的值与正则表达式不相似,则将其视为错误输入。
有很多验证属性可用于围绕模型构建业务规则,但对于我们来说,要实现白名单验证,RegularExpression属性必须发挥作用。
在这个配方中,我们将使用RegularExpression属性为我们的模型属性定义一个模式,以将字符列入白名单。
准备好了吗
通过克隆 ASP.NET Secure Codeing Cookbook 存储库,打开命令 shell 并下载示例网上银行应用:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter01\input-validation\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建我们的示例 OnlineBankingApp 项目及其依赖项。
怎么做…
让我们来看看这个配方的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
打开
Models/Customer.cs文件,在FirstName、MiddleName和LastName模型属性的顶部添加[RegularExpressionAttribute]验证属性,如下代码所示。也包括^[A-Z]+[a-zA-Z]*$表达式:[RegularExpression(@"^[A-Z]+[a-zA-Z]*$", ErrorMessage = "First Name must contain only letters")] [Display(Name = "First Name")] [StringLength(60, MinimumLength = 3)] [Required] public string FirstName { get; set; } [RegularExpression(@"^[A-Z]+[a-zA-Z]*$", ErrorMessage = "Middle Name must contain only letters")] [Display(Name = "Middle Name")] [StringLength(60, MinimumLength = 3)] [Required] public string MiddleName { get; set; } [RegularExpression(@"^[A-Z]+[a-zA-Z]*$", ErrorMessage = "Last Name must contain only letters")] [Display(Name = "Last Name")] [StringLength(60, MinimumLength = 3)] [Required] public string LastName { get; set; } -
在菜单中导航至终端****新终端,或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/Customers/Create。* The browser will display the web page for creating new customers, as shown in the following screenshot:
![Figure 1.1 – Create Customer page]()
图 1.1–创建客户页面
- 在名字文本框中,输入包含数字的输入,然后按选项卡将焦点转移到不同的输入元素。* 验证将启动,并显示一条错误消息:*
- 打开浏览器并转到
*
图 1.2–名字属性验证
通过使用[RegularExpression]属性验证,将验证规则应用于FirstName属性模型,从而限制名字字段中允许的字符。
它是如何工作的…
使用[RegularExpression]验证属性对Customer类的FirstName、MiddleName和LastName属性进行注释,以便根据模式检查这些模型属性值。
^[A-Z]+[a-zA-Z]*$正则表达式模式指定仅允许在这些字段中使用字母,并且第一个字母应为大写格式。这种白名单技术可以防止不良行为人注入恶意输入,并且只允许客户名称使用已知的好字符。
通过在模型中实现验证,我们避免了不必要的代码重复,从而使代码更易于维护,减少了引入安全漏洞的机会。此模型验证是自动执行的,如果输入无效,则安全失败。
笔记
正则表达式是一个复杂的主题,超出了本食谱的范围。要了解有关正则表达式的更多信息,请参阅 Microsoft 官方文档网站上的.NET 正则表达式https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions 。
使用 FluentValidation 库进行白名单验证
大多数 web 开发人员可能希望将验证规则与他们的模型解耦,并希望以单元测试友好的方式编写解决方案。您可能希望创建自己的库来执行白名单验证,或者选择使用流行且易于使用的第三方库,例如具有出色验证功能的FluentValidation。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\input-validation-fluentvalidation\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 键入以下命令以在项目中安装FluentValidation包:dotnet add package FluentValidation- 打开
Startup.cs文件并添加对以下名称空间的引用。这将提供对的FluentValidation类和方法的访问:
using FluentValidation; using FluentValidation.AspNetCore;- 添加对
OnlineBankingApp.Models命名空间的引用:
using OnlineBankingApp.Models;- 在
ConfigureServices方法中,添加对AddFluentValidation和AddTransient方法的调用以配置FluentValidation并添加自定义验证器服务,我们将在下一步创建:
public void ConfigureServices(IserviceCollection services) { services.AddRazorPages().AddFluentValidation(); services.AddTransient<IValidator<Customer>, CustomerValidator>(); //code removed for brevity- 在
Models文件夹中,为我们的自定义验证器创建一个新文件,并将其命名为CustomValidator.cs。* 在CustomValidator.cs中增加以下代码:
using FluentValidation; namespace OnlineBankingApp.Models { public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { RuleFor(x => x.FirstName) .Matches(@"^[A-Z]+[a-zA-Z]*$"); RuleFor(x => x.MiddleName) .Matches(@"^[A-Z]+[a-zA-Z]*$"); RuleFor(x => x.LastName) .Matches(@"^[A-Z]+[a-zA-Z]*$"); } } }- 导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。** 在终端中键入以下命令以构建并运行示例应用:
dotnet run- 打开浏览器并转到
http://localhost:5000/Customers/Create。* 浏览器将显示创建新客户的网页,如图 1.1所示。* 在名字文本框中,输入包含数字的输入,然后按选项卡将焦点转移到不同的输入元素。* 验证将启动,并显示错误消息(参见图 1.2。**
- 打开
**## 它是如何工作的…
FluentValidation是一个服务器端验证框架,允许开发人员使用 lambda 表达式定义验证规则。为了将此库集成到我们的示例网上银行 web 应用中,我们安装了FluentValidation包,并将其添加为我们解决方案的参考:
using FluentValidation.AspNetCore;
对AddFluentValidation扩展方法的调用允许 ASP.NET Core 框架的模型绑定功能使用此库的验证功能:
services.AddRazorPages().AddFluentValidation();
笔记
您仍然可以使用 ASP.NET Core 的内置验证器实现和验证属性,然后将它们与FluentValidations包组合。
然后,我们从AbstractValidator派生出我们自己的CustomerValidator类,这样我们就有了一个类来设置我们的验证规则。在这个 validator 类中,有对FluentValidation包的正则表达式的调用,该正则表达式有一个内置的验证器,用于检查任何名称属性是否包含匹配模式中定义的字母:
RuleFor(x => x.FirstName).Matches(@"^[A-Z]+[a-zA-Z]*$");
RuleFor(x => x.MiddleName).Matches(@"^[A-Z]+[a-zA-Z]*$");
RuleFor(x => x.LastName).Matches(@"^[A-Z]+[a-zA-Z]*$");
我们还调用了AddTransient方法,将自定义验证器添加到收集服务中:
services.AddTransient<IValidator<Customer>,
CustomerValidator>();
AddTransient方法允许 ASP.NET Core 发现CustomerValidator。
还有更多…
前面的两个配方都是服务器端验证的示例。这种类型的验证由 web 服务器上运行的代码执行。对于 ASP.NET Core web 应用(或一般的任何 web 应用),还有另一种执行验证的方法,它在用户代理(通常是 web 浏览器)中执行。Web 开发人员可以使用 HTML5 表单验证或编写自定义 JavaScript 代码编写客户端验证。
搭建 ASP.NET Core web 应用项目时,模板通过立即添加不引人注目的 jQuery 库作为对Pages\Shared\_ValidationScriptsPartial.cshtml的引用,使客户端验证变得容易:
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
我们在 Razor 页面中放置的输入标记帮助程序呈现HTML5 数据-属性*,在浏览器向 web 服务器发送任何请求之前,我们不引人注目的 JavaScript 库将从客户端读取并验证这些属性。此设置简化了将客户端验证添加到 ASP.NET Core web 应用的过程。
提示
不要完全依赖客户端验证,不要信任来自客户端的输入。用户可以通过浏览器的设置禁用 JavaScript,这将阻止客户端验证代码的执行。
另见…
如果您有兴趣了解有关 FluentValidation 库的更多信息,请参阅官方 FluentValidation 库的ASP.NET Core部分:https://docs.fluentvalidation.net/en/latest/aspnet.html 。
句法和语义验证
前面的配方是语法验证的一种形式,我们验证字段结构的正确性(在这种情况下,名称应仅包含字母字符)。
另一种验证类型基于语义,其中输入的有效性取决于特定的业务上下文。
创建自定义验证属性实现语义验证
在语义验证中,进行上下文检查以确保数据符合业务规则。以我们的网上银行应用为例,我们可以定义一条业务规则,规定客户必须拥有一个信誉良好的电子邮件地址,才能创建记录。
在此配方中,您将学习如何使用自定义验证属性执行语义验证。
准备好了吗
在请求免费 API 密钥 https://emailrep.io/key 。EmailRep是一个简单的公共 API,用于检查电子邮件的声誉。一旦您的请求获得批准,您将收到一封来自EmailRep.io的电子邮件,其中包含您的 API 密钥。使用EmailRep.ioweb API 时使用此 API 密钥。
启用机密存储以安全保存我们的 EmailRep API 密钥。打开终端并运行以下命令:
dotnet user-secrets init
然后,运行以下命令:
dotnet user-secrets set "EmailRepApiKey" "key=place-your-api-key-here"
使用 Visual Studio 代码,打开位于\Chapter01\input-validation\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码,通过键入以下命令打开启动练习文件夹:
code . -
在
Services文件夹中,创建一个新文件并将其命名为EmailReputation.cs。在其中,添加对以下名称空间的引用:using System.Net; using System.IO; using System.Text.Json; using Microsoft.Extensions.Configuration; using OnlineBankingApp.Models; -
声明一个
IEmailReputation接口:namespace OnlineBankingApp.Services { public interface IEmailReputation { bool IsRisky(string input); } } -
在相同的
OnlineBankingApp.Services命名空间中,声明一个继承自IEmailReputation接口的EmailReputation类。将服务注入其构造函数public class EmailReputation : IEmailReputation { private readonly IConfiguration Configuration; public EmailReputation(IConfiguration config) { Configuration = config; } } -
Implement the
IsRiskymethod inEmailReputation:public bool IsRisky(string email) { var emailRepApiKey = Configuration["EmailRepApiKey"]; HttpWebRequest repEmailRequest = (HttpWebRequest)WebRequest.Create ($"https://emailrep.io/{email}"); repEmailRequest.Headers.Add("Cookie", $"{emailRepApiKey}"); repEmailRequest.Headers.Add("User-Agent", "MyAppName"); HttpWebResponse repEmailResponse = (HttpWebResponse) repEmailRequest.GetResponse(); Stream newStream = repEmailResponse.GetResponseStream(); var repEmail = new StreamReader(newStream).ReadToEnd(); var reputation = JsonSerializer.Deserialize<Reputation> (repEmail); if (reputation.suspicious || reputation.details.blacklisted || reputation.details.spam || reputation.details.malicious_activity || reputation.details.malicious_activity_recent) return true; return false; }在这里,我们调用了
EmailRep查询 API,并发送 API 密钥和应用名称作为请求头的一部分;也就是说,Cookie和User-Agent。响应是 JSON 格式的。我们可以将其反序列化以检索信誉信息。提示
API 密钥等机密不应在代码或配置文件中硬编码。泄露这些敏感信息只需要一个有权访问代码存储库的受损帐户。开发人员应将密码和凭据存储在更安全的环境中,如 Azure 密钥库或 AWS 密钥管理服务。
在这个配方中,我们使用秘密管理工具在开发过程中安全地保存
EmailRepAPI 密钥。要了解有关 secret manager 工具以及如何存储您的机密的更多信息,请参阅 Microsoft 文档中 ASP.NET Core 中开发中的应用机密的安全存储部分 https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0&选项卡=窗口。 -
导航到
OnlineBankingApp项目的Model文件夹,创建一个名为ReputableEmailAttribute.cs的新文件,开始创建自定义验证属性。 -
将以下命名空间引用添加到
ReputableEmailAttribute.cs文件:using System.ComponentModel.DataAnnotations; using System.Net; using System.IO; using System.Text.Json; -
Declare a
Reputationand aDetailsclass:public class Details { public bool blacklisted { get; set; } public bool malicious_activity { get; set; } public bool malicious_activity_recent { get; set;} public bool spam { get; set; } public bool suspicious_tld { get; set; } } public class Reputation { public Details { get; set; } public string email { get; set; } public string reputation { get; set; } public bool suspicious { get; set; } }两个类都将保存我们从
EmailRep.ioAPI 收到的响应中的反序列化信息。笔记
EmailRep.ioAPI 的响应包含比我们在Reputation和Details类中定义的属性更多的信息。我们已经使用了这个食谱中最基本的东西,但是如果你愿意,你可以自由加入更多。 -
在继承自
ValidationAttribute并定义GetErrorMessage属性的OnlineBankingApp.Models命名空间中声明另一个名为ReputableEmailAttribute的类:namespace OnlineBankingApp.Models { public class ReputableEmailAttribute : ValidationAttribute { public string GetErrorMessage() => "Email address is rejected because of its reputation"; } } -
将的
ValidationAttribute的IsValid方法改写为以下代码:
```cs
protected override ValidationResult IsValid(object value,ValidationContext validationContext)
{
string email = value.ToString();
var service = (IEmailReputation) validationContext.GetService (typeof(IEmailReputation));
if (service.IsRisky(email))
return new ValidationResult(GetErrorMessage());
return ValidationResult.Success;
}
```
- 修改
Models/Customer.cs文件,并使用新的自定义[ReputableEmail]验证属性
```cs
[ReputableEmail]
[Display(Name = "Email Address")]
[Required]
[EmailAddress]
public string Email { get; set; }
```
注释`Email`属性
- 导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建和运行示例应用:
```cs
dotnet run
```
* 打开浏览器并转到`http://localhost:5000/Customers`。* 浏览器将显示由种子数据生成的客户列表。* Select one record and click its **Edit** link:

图 1.3–编辑客户页面
* 您将被重定向到**编辑**页面,在该页面中您可以修改**客户**的详细信息。* 更改当前的**电子邮件地址**字段,并将其值设置为`email@test.xyz`。* 点击**保存**。注意出现的错误消息,表明电子邮件已被拒绝:*
*
图 1.4–电子邮件地址属性验证
通过创建自定义验证属性并注释电子邮件地址,我们现在可以在客户模型中执行语义验证。建议编写适当的自定义模型验证器,因为它在创建验证规则时提供了更多的控制和灵活性。
它是如何工作的…
我们创建了一个新类,该类继承自ValidateAttribute并重写其IsValid方法。为了实施语义验证,我们创建了一个服务,我们的自定义属性将使用该服务调用EmailRep 查询 API,并确定其EmailAddress是否与任何已知的恶意活动相关,标记为垃圾邮件、可疑或黑名单:
if (reputation.suspicious || reputation.details.blacklisted || reputation.details.spam || reputation.details.malicious_activity || reputation.details.malicious_activity_recent)
return new ValidationResult(GetErrorMessage());
return ValidationResult.Success;
这种类型的验证(电子邮件地址因其声誉而被拒绝)可能是您所熟悉的。这种验证称为黑名单。黑名单验证是您可以在 ASP.NET Core web 应用中使用的验证策略之一。
糟糕的验证会使 ASP.NET Core web 应用面临不必要的安全风险。在应用中实现足够的验证策略以覆盖语法和语义规则,从而减少注入和逻辑漏洞的机会,这一点至关重要。使用内置验证框架或可信的第三方验证库(如 FluentValidation)是很好的对策。
笔记
许多其他公司提供相同的电子邮件声誉服务,该服务提供评分并评估电子邮件地址的风险。根据您的要求使用可靠的 API 服务,并需要在将记录保存到数据库之前检查电子邮件地址的完整性。
输入消毒
开发人员在处理输入时可以实现的另一个补充策略是从数据中删除或替换不需要的字符。您的应用可能需要一些自由格式的文本或 HTML 格式的输入,为了避免利用此向量的攻击,您必须执行清理。
您可以编写自己的清理方法,并与输入验证类似,实现修改输入的白名单或黑名单方法。
在本食谱中,您将学习如何编写自己的代码来进行消毒输入。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\input-sanitization\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
打开
Models/FundTransfer.cs文件,添加对System.Text.RegularExpressions名称空间的引用:using System.Text.RegularExpressions; -
修改
Note模型属性[StringLength(60)] [DataType(DataType.MultilineText)] public string Note { get => note; set => note = Regex.Replace(value, @"[\!\@\$\%\^\&\<\>\?\|\;\[\]\{\~]+" , string.Empty); }中的代码
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在 Visual Studio 代码终端中,键入以下命令以生成并运行示例应用:dotnet run- 打开浏览器,进入
http://localhost:5000/FundTransfers/Create。* 通过下拉列表选择要转帐的帐户。* 输入要转账的金额。* In the Notes multi-text field, attempt to exploit the app by entering a malicious script tag (that is,<script>alert()</script>), as shown in the following screenshot:
![Figure 1.5 – Malicious input in the Note field]()
图 1.5–注释字段中的恶意输入
- 点击发送。* 示例应用会将您重定向到资金转账页面,您将看到最近创建的资金转账记录。* 请注意,小于和大于字符已从注释部分中删除,现在已被消毒:*
- 打开浏览器,进入
*
图 1.6–消毒注释值
我们可以将[\!\@\$\%\^\&\<\>\?\|\;\[\]\{\~]+正则表达式作为Regex.Replace方法参数之一传递,指定要删除与此模式匹配的不需要的字符。此方法将注释输入值从<script>alert()</script>更改为scriptalert()/script,使得渲染输出更加安全。
它是如何工作的…
我们的样本网上银行应用有一个资金转账页面,允许用户在账户之间转账。其中一个表单字段是注释,它允许用户输入用 HTML 标记格式化的自由格式文本。
我们修改了Note模型属性的集合评估器,以清理分配给它的值,以防止出现不需要的字符或标记,例如<script>标记。
我们添加了对System.Text.RegularExpressions名称空间的引用,以便使用Regex.Replace方法清理输入。使用Regex.Replace方法,我们指定了一个正则表达式模式,以便在值中查找;如果找到匹配项,我们将其替换为空字符串。
使用 HTMLSanitizer 库输入消毒
还有其他的开源库在净化输入方面做得很好,其中之一就是HTMLSanitizer。
在本食谱中,您将学习如何使用HTMLSanitizer第三方库对输入进行消毒。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\input-sanitization-htmlsanitizer\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 键入以下命令在您的项目中安装软件包:dotnet add package HtmlSanitizer- 打开
Models/FundTransfer.cs文件,添加对Ganss.XSS的引用,即HtmlSanitizer名称空间:
using Ganss.XSS;- 修改
Note模型属性设置器中的代码:
[StringLength(60)] [DataType(DataType.MultilineText)] public string Note { get => note; set => note = new HtmlSanitizer().Sanitize(value); }- 在 Visual Studio 代码终端中,键入以下命令以生成并运行示例应用:
dotnet run- 打开浏览器并转到
http://localhost:5000/FundTransfers/Create。* 通过下拉列表选择要从和转账的账户。* 输入要转账的金额。* In the Notes multi-text field, enter<b>Contingency Fund<b> <script>alert()</script>. This is a string that's formatted with a bold tag that has a malicious script tag next to it, as shown in the following screenshot:
![Figure 1.7 – HTML formatted input in the Note field]()
图 1.7–注释字段中的 HTML 格式输入
- 点击发送。* 示例应用会将您重定向到资金转账页面,您将看到最近创建的资金转账记录。* 请注意,应急基金字符串的格式为粗体,但脚本标记已完全删除:*
- 打开
*
图 1.8–格式化和消毒注释值
使用HtmlSanitizerNuGet 包,我们可以清理用户控制的输入,并防止我们的示例解决方案存在XSS漏洞。
它是如何工作的…
为了在我们的示例网上银行应用中为我们的注释提供安全的 HTML 格式,我们必须清理输入,使其不包含任何有害的标记和属性。HTMLSanitizer帮助我们完成此任务,内置白名单机制,只允许特定的 HTML 或 CSS 标签和属性。
前面的代码显示Sanitize方法从输入字符串中去除了不在白名单中的其余标记或属性。
默认情况下,HTMLSanitizer已经提供了安全标签列表。您可以创建自己的白名单,但在我们的示例中,我们不需要设置Sanitizer实例的AllowTags或AllowAttribute属性;只需调用Sanitize方法并传递值即可。
输出编码
输出编码或逃逸仍然是另一种有助于中和注入攻击的防御技术。此过程将替换不受信任数据中的字符,从而允许应用在其适当的上下文中安全地显示输出。
在 ASP.NET Core web 应用中,开发人员应该了解不同的上下文输出,以了解在给定上下文中使用的正确编码器。这些是HTML、HTML 属性上下文、CSS 上下文和JavaScript 上下文。
默认情况下,ASP.NET Core 中的 Razor 引擎会自动转义输出,除了少数例外情况,其中一个方法会禁用这种编码。ASP.NET Core 还提供了多种编码器,我们可以使用它们显式实现适当的上下文输出。
在接下来的几个食谱中,我们将学习如何使用HtmlEncoder、UrlEncoder和JavascriptEncoder执行输出编码。
使用 HtmlEncoder 进行输出编码
HTML 编码转换特殊字符,以便浏览器将正确解释文本,而不是将其呈现为 HTML。例如,字符串可能包含小于字符<,在 HTML 标准中,这是用于打开和关闭标记的 HTML 实体。这需要转义到<中以保留文本的含义。
转义输出提供的保护在于防止攻击者在解释器解析输入时更改其意图或目的。这将阻止恶意参与者尝试在 HTML 上下文中执行脚本。
下表显示了最常见的 HTML 实体及其编码对应项。这绝不是一份完整的清单:

表 1.1–HTML 实体
在本食谱中,您将学习如何使用HtmlEncoder以 HTML 格式转义输出。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\output-encoding-html\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们看一下这个配方的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
打开
Pages\FundTransfers\Index.cshtml文件,在 Razor 页面添加@inject指令,注入HtmlEncoder服务:@inject System.Text.Encodings.Web.HtmlEncoder htmlEncoder -
将
Note表格数据单元格内的标记替换为以下代码:@if (item.Note is not null) { @(new Microsoft.AspNetCore.Html.HtmlString (htmlEncoder.Encode(item.Note))) } -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在 Visual Studio 终端中,键入以下命令以生成并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/FundTransfers。* 通过右键单击页面中的任意位置并从浏览器上下文菜单中选择查看页面源来查看呈现的 HTML。* 请注意带有脚本标记的注释已被 HTML 编码的标记:
<td>Contingency Fund <script>alert()</script> </td> ```* - 打开浏览器并转到
*## 它是如何工作的…
System.Text.Encodings.Web名称空间引入了大量字符编码器,开发人员可以使用这些编码器转义输出。其中之一是HtmlEncoder,它将帮助我们对数据进行 HTML 编码。
通过依赖注入,我们可以使用@inject指令添加HtmlEncoder对象:
@inject System.Text.Encodings.Web.HtmlEncoder htmlEncoder
htmlEncoder变量将保存HtmlEncoder对象的一个实例,通过其Encode方法,我们可以从FundTransfer对象传入Note属性的值进行转义:
htmlEncoder.Encode(item.Note)
Encode方法转换item.Note的值,现在将成为编码字符串。
使用 URLCoder 进行输出编码
URL 编码将输出中的字符转换为 ASCII 格式。它还将不安全字符替换为%字符作为前缀,然后添加两个十六进制数字。
以下是字符及其 URL 编码等价物的部分列表:

表 1.2——编码字符及其等价物的百分比
在本食谱中,您将学习如何使用UrlEncoder转义 URL。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\output-encoding-url\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个配方的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
打开
Pages\FundTransfers\Index.cshtml文件,在 Razor 页面添加@inject指令,注入UrlEncoder服务:@inject System.Text.Encodings.Web.UrlEncoder urlEncoder -
将
Note表格数据单元格内的标记替换为以下代码:<td> <a asp-page="./Create" asp-route-id="@item.ID" asp-fragment="@(item.Note is null ? string.Empty : urlEncoder.Encode(item.Note))" >Send Again</a> </td> -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在 Visual Studio 终端中,键入以下命令以生成并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/FundTransfers。* 通过右键单击页面中的任意位置并从浏览器上下文菜单中选择查看页面源来查看呈现的 HTML。* 请注意,Send Again的超链接及其脚本标记被 URL 编码为
<td> <a href="/FundTransfers/Create?id=1#Contingency%20 Fund%20%3Cscript%3Ealert()%3C%2Fscript%3E">Send Again</a> </td>的标记*
- 打开浏览器并转到
*片段中的文本现在已按百分比编码,因此将潜在的恶意字符(如<和 >)替换为%3C和%3E。
它是如何工作的…
asp-fragment属性被分配Note模型属性值,作为一条持续信息发送到创建新资金转账页面:
asp-fragment="@(item.Note is null ? string.Empty : item.Note)
在不编码item.NoteURL 片段的情况下,在目标页面中处理此数据的代码可以选择此值并按文本形式解析它。处理未扫描数据的风险在于,解释器可能会将其作为代码,并基于错误的上下文执行它。
我们可以使用UrlEncoder对象的Encode方法来转义Note模型属性值,以防止这种情况发生。
使用 JavascriptEncoder 进行输出编码
没有 JavaScript,Web 开发就不可能完成。它一直是开发 web 应用的事实上的脚本语言,用于多种用途,从动画到验证客户端的输入。在某些情况下,开发人员会将 JavaScript 代码块与 C 代码或 ASP.NET Core 页面中的 Razor 语法混合使用。这种方法使得在 JavaScript 旁边转义 outp`ut 成为必要,并且还可以防止 JavaScript 代码被注入恶意函数。
在本教程中,您将学习如何使用JavascriptEncoder在 JavaScript 中转义输出。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\output-encoding-js\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
打开
Pages\Customers\Index.cshtml文件,在 Razor 页面添加@inject指令,注入JavascriptEncoder服务:@inject System.Text.Encodings.Web.JavaScriptEncoder jsEncoder -
将
@section Scripts替换为以下代码:@section Scripts { <script type="text/javascript"> $(document).ready(function() { @foreach (var item in Model.Customer) { <text> var $tr = $('<tr>').append ($('<td>').text("@jsEncoder .Encode(item.FirstName)")); $tr.append($('<td>').text ("@jsEncoder.Encode(item .MiddleName)")); $tr.append($('<td>').text ("@jsEncoder.Encode (item.LastName)")); $tr.append($('<td>') .text("@jsEncoder.Encode (item.DateOfBirth.ToString ("d"))")); $tr.append($('<td>').text ("@jsEncoder.Encode (item.Email)")); $tr.append($('<td>').text ("@jsEncoder.Encode (item.Phone)")); $tr.appendTo('#table'); </text> } }); </script> } -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在 VisualStudio 终端中,键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/Customers。* 通过右键单击页面中的任意位置并在浏览器上下文菜单中选择查看页面源来查看呈现的 HTML。* 请注意,查看详细信息的超链接及其脚本标记已被 URL 编码的标记:
var $tr = $('<tr>').append($('<td>').text("Dylan \u003C/script\u003E\u003Cscript\u003Ealert()\u003C/script\u003E")); ```* - 打开浏览器并转到
*模型属性值中的字符现在是 JavaScript 编码的,从而防止坏角色利用输出和注入任意 JavaScript 代码。
它是如何工作的…
在script部分中,我们有 jQuery 代码,可以动态生成 HTML 表的内容。循环遍历Customer集合的每个项并将其呈现到每个表的单元格:
$(document).ready(function() {
@foreach (var item in Model.Customer) {
<text>
...code removed for brevity
</text>
}
});
</script>
}
在 JavaScript 上下文中,我们使用JavascriptEncoder对象并调用Encode方法对模型中的所有数据进行编码。
使用数据保护 API 保护敏感数据
ASP.NET Core 安全编码技术的一部分应该包括在静止状态下保护应用的敏感数据,这是没有问题的。个人身份信息(PII)、被归类为机密的数据以及可列举的密钥和 ID 应加密。ASP.NET Core 通过在其提供简化 API 的框架中开发数据保护堆栈,使开发人员能够轻松实现这一点。
在本教程中,您将学习如何使用数据保护 API(DPAPI)来保护 ASP.NET Core web 应用中暴露敏感数据的部分。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter01\data-protection\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
打开
Pages\Customers\Index.cshtml.cs文件,添加对Microsoft.AspNetCore.DataProtection名称空间的引用:using Microsoft.AspNetCore.DataProtection; -
将 Data Protector 接口添加为私有成员:
private readonly IDataProtector _dataProtector; -
修改
IndexModel页面模型,使其包含构造函数的附加参数:public IndexModel(OnlineBankingApp.Data .OnlineBankingAppContext context, IDataProtectionProvider dataProtector) { _context = context; _dataProtector = dataProtector.CreateProtector ("OnlineBankingApp.Pages.Customers"); } -
在
Customer类[NotMapped] public string EncCustomerID { get; set; }中添加一个新的
EncCustomerID属性 -
使用
Protect方法public async Task OnGetAsync() { foreach (var cust in _context.Customer) { cust.EncCustomerID = _dataProtector.Protect(cust.ID.ToString()); } Customer = await _context.Customer.ToListAsync(); }加密
Customer类的 ID 属性 -
更改 jQuery 代码动态生成锚定标签的行:
$tr.append($('<td>').append("<a href='/Customers/Details?id=@item.EncCustomerID'>See Details</a>")); -
打开
Pages\Customers\Details.cshtml.cs文件,添加对Microsoft.AspNetCore.DataProtection名称空间的引用:using Microsoft.AspNetCore.DataProtection; -
将 Data Protector 接口添加为私有成员:
private readonly IDataProtector _dataProtector; -
修改页面模型中的
DetailsModel页面,使其包含一个额外的构造函数参数:
```cs
public DetailsModel (OnlineBankingApp.Data.OnlineBankingAppContext context, IDataProtectionProvider dataProtector)
{
_context = context;
_dataProtector = dataProtector.CreateProtector ("OnlineBankingApp.Pages.Customers");
}
```
- 更改
OnGetAsync方法:
```cs
public async Task<IActionResult> OnGetAsync(string id)
{
if (id == null)
{
return NotFound();
}
var decID = Int32.Parse(_dataProtector.Unprotect(id));
Customer = await _context.Customer .FirstOrDefaultAsync(m => m.ID == decID);
```
- 打开一个浏览器,进入
http://localhost:5000/Customers。 - 通过右键单击页面中的任意位置并从浏览器上下文菜单中选择查看页面源来查看呈现的 HTML。
- 请注意 jQuery 代码的两行,其中指向详细信息页面的超链接不再显示查询字符串参数
```cs
$tr.append($('<td>').append("<a href='/Customers/Details?id=CfDJ8CsNdycKdZtHo72FYN-pXqvrK1k8Z-c4FPe7huOeyCazSmmHbF8fUaQAbio0JpDxOcg9J4-voevmBcHwpBsJWx77ZG5vhpzkLnGB8m13uBo5BLiIdsl2Epk9kj97d5PRJw'>See Details</a>"));
...
$tr.append($('<td>').append("<a href='/Customers/Details?id=CfDJ8CsNdycKdZtHo72FYN-pXquGL7YVUQfASM5cVvyol-OK-xQyErXGit9Kdgs6YyBBdEcNtoqq9c7kqr1J7EzkI0zszL-700OTcVgXvqY4wdyseN-2uESydCdv-KOqOXboLg'>See Details</a>"));
```
中的实际**客户 ID**
href属性现在已替换为一个 URL,该 URL 为id查询字符串参数附加了一个不同的值。此参数现在具有加密形式的客户 ID。
它是如何工作的…
我们的客户 ID最初在锚定标记中作为查询字符串参数值公开,可以使用浏览器开发工具轻松查看。这使得应用容易受到基于枚举类型的攻击,因此我们需要保护此数据免受窥探:
<a href="/Customers/Details?id=1">See Details</a>
笔记
通常,数字类型作为主键已经成为过去,企业数据库设计不再使用数字 ID,而是使用全局唯一标识符(GUID)。但为了简化起见,我们可以使用整数来理解使用可猜测键的风险。
我们可以利用DPAPI提供的数据保护服务和加密此信息,然后再在页面上呈现链接。
首先添加对Microsoft.AspNetCore.DataProtection的引用,并通过将此服务注入我们的pagemodel来利用IDataProtectionProvider:
DetailsModel(OnlineBankingApp.Data.OnlineBankingAppContext context, IDataProtectionProvider dataProtector)
将此接口添加到构造函数允许我们调用CreateProtector方法,以便创建IDataProtector对象的实例。IDataProtector所需的参数之一是唯一的目的字符串,我们可以使用它来加密和解密有效负载:
_dataProtector = dataProtector.CreateProtector ("OnlineBankingApp.Pages.Customers");
我们将使用OnlineBankingApp.Pages.Customers作为Index和Detail模型的目的字符串来破译客户 ID,这是我们试图保护的信息。目的字符串创建隔离,防止DetailsModel和IndexModel以外的使用者解密受保护的数据。
我们需要添加一个新属性来保存加密的客户 ID,因此我们必须将EncCustomerID定义为字符串数据类型。我们将使用[NotMapped]对其进行注释,以防止 Entity Framework 为此新属性创建列:
[NotMapped]
public string EncCustomerID { get; set; }
最后,我们必须遍历所有客户,保护他们的客户 ID,并将其分配给EncCustomerID属性:
foreach (var cust in _context.Customer)
{
cust.EncCustomerID = _dataProtector.Protect(cust.ID.ToString());
}
需要时从DetailsModel添加UnProtect。在这里,我们必须解密受保护的数据以从数据库中提取信息:
var decID = Int32.Parse(_dataProtector.Unprotect(id));
如果_dataProtector是用不同的目的字符串实例化的,则会抛出一个CryptographicException;否则,密文的解密将成功。
另见
前面的方法是一个简单的示例,说明如何快速实现DPAPI并保护 ASP.NET Core web 应用中的数据。要了解 DPAPI 背后的原理,并深入了解 API,请参阅 Microsoft 官方文档中的中的ASP.NET Core 数据保护https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-5.0。*********
二、注入缺陷
代码中的注入缺陷可能对 ASP.NET Core web 应用产生最严重的影响。由于缺乏对不受信任输入的验证和清理,因此可以利用此漏洞,导致执行任意操作系统命令、绕过身份验证、意外的数据操作和内容。更糟糕的是,它可能会泄露敏感信息,最终导致数据泄露。
本章向您介绍各种注入缺陷,并解释如何在代码中修复此安全缺陷。
在本章中,我们将介绍以下配方:
- 用实体框架修复 SQL 注入
- 修复 ADO.NET 中的 SQL 注入
- 固定 NoSQL 注入液
- 固定命令注入
- 修复 LDAP 注入
- 修复 XPath 注入
在本章结束时,您将学习如何正确编写安全代码并删除可防止注入攻击的安全漏洞。
技术要求
本书是为配合 Visual Studio 代码、Git 和.NET 5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。这些代码练习已经在 Windows 环境中进行了测试,但应该可以在基于 Linux 的操作系统中运行,但有一些细微的差异。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。MongoDB 需要能够测试 NoSQL 注入剂的配方。还需要一个可以打开和浏览 SQLite 数据库的工具,如 SQLite 的 DB Browser。您可以在找到本章的代码 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter02 。
什么是 SQL 注入?
ASP.NET Core web 应用与数据库交互以存储数据和记录。我们使用标准查询语言与数据库管理系统(DBMS)进行通信访问和管理数据。这些查询是使用编程语言、平台或所选库组成的,但是生成这些查询的代码可能编写得不安全。
开发人员可以编写生成动态 SQL 的代码,方法是将字符串与不受信任的用户输入连接起来。如果没有适当的对策,恶意参与者可以将可疑命令注入输入字符串,从而改变查询的意图,或者执行任意 SQL。这种代码中的漏洞被识别为SQL 注入,至今仍在 web 应用中普遍存在。
用实体框架修复 SQL 注入
实体框架核心(EF Core)是 ASP.NET Core 开发人员首选的对象关系映射(ORM)框架。此框架是跨平台的,其易用性允许开发人员将数据即时建模和查询到对象中。然而,像 EF Core 这样的 ORM 框架仍然可能被误用。
在此配方中,我们将执行一个简单的 SQL 注入,以利用该漏洞,定位安全漏洞,并通过重写更安全的代码版本来修复风险。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter02\sql-injection\razor\ef\before\OnlineBankingApp\的示例网上银行应用文件夹。
测试 SQL 注入
以下为步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/FundTransfers。* The browser will display the web page for searching fund transfers using keywords in the Filter By Notes field, as shown in the following screenshot:
系统。
![Figure 2.1 – Fund Transfers page]()
图 2.1–资金转账页面
- 在按注释过滤文本框中,键入
C,然后点击搜索按钮。* The web page will now return one entry finding one match for the Contingency Fund note:
![Figure 2.2 – Fund Transfers search result]()
图 2.2–资金转账搜索结果
- 现在尝试输入 SQL 注入负载:
%';create table tbl1(one varchar(10), two smallint);Select * from Customers where id like '1。* Notice that no error was thrown on the web page:
![Figure 2.3 – Successful SQL injection]()
图 2.3–成功的 SQL 注入
- 要确认 SQL 注入有效负载已成功执行,请使用 SQLite 工具的DB Browser 打开
\Chapter02\sql-injection\razor\ef\before\OnlineBankingApp\OnlineBank.dbSQLite 数据库:*
- 打开浏览器并转到
*
图 2.4–SQLite 数据库浏览器中的 OnlineBank.db
注意新创建的tbl1SQLite 表
现在,让我们看看如何识别使用 EF 的代码中的 SQL 注入漏洞,并通过修复此安全漏洞和应用对策来缓解上述问题。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令来构建示例应用,以确认没有编译错误:dotnet build- 打开
Pages/FundTransfers/Index.cshtml.cs文件,定位OnGetAsync方法的易受攻击部分,其中包含一个动态查询:
public async Task OnGetAsync() { var fundtransfer = from f in _context.FundTransfer select f; if (!string.IsNullOrEmpty(SearchString)) { fundtransfer = _context.FundTransfer. FromSqlRaw("Select * from FundTransfer Where Note Like'%" + SearchString + "%'"); } FundTransfer = await fundtransfer.ToListAsync(); }- 为了修复 SQL 注入漏洞,让我们从开始,添加对
System的引用。* 接下来,使用FromSqlInterpolated方法将前面突出显示的代码更改为以下代码:
fundtransfer = _context.FundTransfer.FromSqlInterpolated($"Select * from FundTransfer Where Note Like {"%" + SearchString + "%"}");FromSqlInterpolated方法将根据提供的插值字符串创建 LINQ 查询。*
- 打开
*插入的参数SearchString随后将转换为DbParameter对象,使代码不受 SQL 注入的影响。
它是如何工作的…
实体框架允许您使用FromSQLRaw方法执行原始 SQL 查询。但是,此方法是危险的,因为您可以使用用户输入SearchString提供带有串联字符串的参数:
_context.FundTransfer.FromSqlRaw("Select * from FundTransfer Where Note Like'%" + SearchString + "%'");
使用 SQL 注入测试中使用的有效负载,想象一下用恶意字符串%';create table tbl1(one varchar(10), two smallint);Select * from Customers where id like '1替换SearchString值。
通过FromSqlRaw盲目连接注入的输入,SQL 语句现在的内容如下:
Select * from FundTransfer Where Note Like'%%';create table tbl1(one varchar(10), two smallint);Select * from Customers where id like '1 %'
这是一个非常有效的系列 SQL 语句,除了有一个危险的命令,该命令会创建一个新表,或者在其他情况下,或者在 DBMS 中,可能通过生成一个 shell 而变成远程代码执行。
这种形成 SQL 语句的方法被认为是糟糕的编码实践。为了编写更好、更安全的代码,可以使用FromSqlInterpolated等方法帮助编写带有参数化值的无害 SQL 语句。
还有更多…
参数化是一种经过验证的安全编码实践,可以防止 SQL 注入。重写此配方中代码的另一种方法是使用DbParameter类。
在代码中引入SqLiteParameter(源于DbParameter的实例)如下:
var searchParameter = new SqliteParameter("searchString", SearchString);
fundtransfer = _context.FundTransfer .FromSqlRaw("Select * from FundTransfer Where Note Like'%@searchString%'",searchParameter);
白名单也是一种非常有用的技术,可以过滤用户输入。您已经在第 1 章、安全编码基础中详细讨论了这种方法。白名单将导致 ASP.NET Core web 应用仅处理预期格式的数据,但这种技术不如使用准备好的语句或参数化查询有效。
修复 ADO.NET 中的 SQL 注入
ADO.NET是一个数据提供程序平台,是.NET 框架的一部分。自.NET Framework 问世以来,ADO.NET 一直是用于查询和操作数据库中数据的组件。ADO.NET 可用于开发数据驱动的 ASP.NET Core web 应用,但与任何数据提供程序类似,开发人员在使用任何System.Data.*或Microsoft.Data.*类时可能会编写不安全的代码。
在此配方中,我们将在使用 ADO.NET 时识别代码中的 SQL 注入漏洞,并通过修复此安全漏洞和应用对策来缓解此问题。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter02\sql-injection\razor\ado.net\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令来构建示例应用,以确认没有编译错误:dotnet build- 打开
Data/FundTransferDAL.cs文件,该类表示示例应用的数据访问层,并找到GetFundTransfers方法的易受攻击部分,其中用户控制的输入被传递到搜索参数:
public IEnumerable<FundTransfer> GetFundTransfers(string search) { List<FundTransfer> fundTransfers = new List<FundTransfer>(); using (SqliteConnection con = new SqliteConnection(connectionString)) { SqliteCommand cmd = new SqliteCommand("Select * fromFundTransfer where Note like '%" + search + "%'", con); cmd.CommandType = CommandType.Text; con.Open(); SqliteDataReader rdr = cmd.ExecuteReader();- 前面突出显示的代码是组成查询的地方,搜索连接起来形成 SQL 查询。* 要修复 SQL 注入漏洞,请更改前面突出显示的代码:
public IEnumerable<FundTransfer> GetFundTransfers(string search) { List<FundTransfer> fundTransfers = new List<FundTransfer>(); using (SqliteConnection con = new SqliteConnection(connectionString)) { SqliteCommand cmd = new SqliteCommand("Select * from FundTransfer where Note like '%" + @search + "%'", con); cmd.CommandType = CommandType.Text; cmd.Parameters.AddWithValue("@search",search); con.Open(); SqliteDataReader rdr = cmd.ExecuteReader(); ```* - 打开
*使用参数化方法,我们将搜索字符串转换为 SQL 参数,并将值传递到SqlLiteParameterCollection。
它是如何工作的…
SqlLiteCommand实例被盲目地传递一个原始的 SQL 连接用户输入。此提供的字符串是 SQL 注入的源。输入字符串搜索未经验证和未初始化,允许对手插入任意 SQL 命令或修改查询意图:
SqliteCommand cmd = new SqliteCommand("Select * from FundTransfer where Note like '%" + search + "%'", con);
您可以重写易受攻击的 ADO.NET 代码,并通过使用查询参数使其安全。SQliteCommand对象SqliteParametersCollection中的AddWithValue方法允许您添加查询参数并安全地将值传递到查询中:
cmd.Parameters.AddWithValue("@search", search);
将搜索字符串更改为占位符将使查询参数化:
SqliteCommand cmd = new SqliteCommand("Select * from FundTransfer where Note like '%" + @search + "%'", con);
当 ASP.NET Core web 应用执行前面几行代码时,查询现在被参数化,安全地传递搜索值,并防止恶意参与者更改 SQL。
还有更多…
此配方使用SQLite作为样本解决方案的 DBMS,但如果要使用Microsoft SQL Server,另一个选项是将查询转换为存储过程并与 DB 参数一起使用。然后,您必须使用SQLCommand对象并将CommandType属性设置为System.Data.CommandType.StoredProcedure,从而允许从代码执行参数化存储过程。这些类在System.Data.SqlClient名称空间和新的Microsoft.Data.SqlClient包中提供。
下面是一个示例代码片段:
SqlCommand cmd = new SqlCommand("sp_SearchFundTransfer",con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@search", search);
要编写更好、更安全的代码,请使用内置的对数据库功能的支持,例如由其数据提供程序框架提供的准备语句或参数化查询。
固定 NoSQL 注入液
NoSQL数据库是一种不同类型的数据库,其中存储非关系和半结构化数据。NoSQL 数据库有很多种,比如 Cassandra、Redis、DynamoDB 和 MongoDB,每种数据库都有自己的查询语言。尽管这些查询彼此不同,但它们也容易受到注入攻击。
在本配方中,我们将识别使用 MongoDB 作为后端的代码中的NoSQL 注入漏洞,并通过应用多种对策来修复该问题。
准备好了吗
使用 Visual Studio 代码,打开位于Chapter02\nosql-injection\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令来构建示例应用,以确认没有编译错误:dotnet build- 打开
Services/PayeeService.cs文件,在Get(string name)方法
public List<Payee> Get(string name) { var filter = "{$where: \"function() {return this.Name == '" + name + "'}\"}"; return payees.Find(filter).ToList(); }中定位代码的易受攻击部分* 要修复 NoSQL 注入漏洞,请更改前面突出显示的代码:
public List<Payee> Get(string name) { return payees.Find(payee => payee.Name == name).ToList(); } ```* - 打开
*传递到Find方法中的过滤器现在被Lambda表达式替换,这是一种更安全的按姓名搜索收款人的方法。
它是如何工作的…
Get方法有一个字符串参数,可以为该参数提供非净化或验证值。这个值可以改变由它组成的MongoDB过滤器,使 NoSQL 数据库执行意外行为。
name参数可以附加一个表达式,该表达式将把查询计算为与查询预期执行的结果不同的逻辑结果。JavaScript 子句也可以插入到查询中,该查询可以终止语句并添加新的任意代码块。
通过一些一般建议,避免使用$where操作符。只需将一个 C#Lambda 表达式作为过滤器应用,以防止任何可注入的 JSON 或 JavaScript 表达式。
还有更多…
假设前面的选项不可能,并且必须使用$where子句,那么必须使用 JavaScript 对输入进行编码。使用System.Text.Encodings.Web命名空间中的JavaScriptEncoder类来编码传递到参数中的值:
-
首先,修改
PayeeService.cs文件,添加对Encoder名称空间的引用:using System.Text.Encodings.Web; -
接下来,为
JavaScriptEncoderprivate readonly JavaScriptEncoder _jsEncoder;定义一个属性
-
更改
PayeeService构造函数,添加新参数注入JavaScriptEncoder:public PayeeService(IOnlineBankDatabaseSettings settings,JavaScriptEncoder jsEncoder) -
最后,使用
JavaScriptEncodervar filter = "{$where: \"function() {return this.Name == '" + _jsEncoder.Encode(name) + "'}\"}";的
Encode功能对name参数进行编码
如果恶意输入被传递到name参数,并通过Encode方法转义,如果转义值无法解释为有效的 JavaScript 表达式,C#MongoDB 驱动程序将抛出异常。
为了防止NoSQL 注入,开发人员必须避免使用字符串连接构建动态查询。NoSQL 数据库提供了查询和处理数据的方法,但您必须了解一项功能可能会给 ASP.NET Core web 应用带来的潜在安全隐患。
固定命令注入
Web 应用如使用 ASP.NET Core 开发的应用,有大量的组件和库,使它们能够在主机上执行 OS 命令。如果编写得不安全,编写和运行这些命令的代码可能会使 ASP.NET Core web 应用遭受命令注入攻击。如果不防止代码中的此安全缺陷,则可能意外执行 Shell 命令。
在此配方中,我们将识别代码中的命令注入漏洞,并修复安全漏洞。
准备好了吗
使用 Visual Studio 代码,打开位于Chapter02\command-injection\before\OnlineBankingApp的示例网上银行应用文件夹。
测试命令注入
以下为步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/Backups/Create。* The browser will display the web page for initiating database backup, as shown in the following screenshot:
![Figure 2.5 – Backup page]()
图 2.5–备份页面
- 在备份名称字段中输入此命令注入有效负载
backup & calc,并点击创建按钮。* 请注意,页面已重定向到备份页面列表,备份已创建。但是,计算器应用出现了:*
- 打开浏览器并转到
*
图 2.6–成功的命令注入
如果不处理此安全漏洞,此问题还可能使底层主机暴露于远程代码执行(RCE。
怎么做…
让我们来看看这个配方的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令来构建示例应用,以确认没有编译错误:dotnet build- 打开
Services/BackupService.cs文件,在BackupDB(string backupname)方法
public async Task BackupDB(string backupname) { using (Process p = new Process()) { string source = Environment.CurrentDirectory + "\\OnlineBank.db"; string destination = Environment.CurrentDirectory + "\\backups\\" + backupname; p.StartInfo.Arguments = " /c copy " + source + " " + destination; p.StartInfo.FileName = "cmd"; p.StartInfo.CreateNoWindow = true; ...code removed for brevity中定位代码的易受攻击部分* 要修复命令注入漏洞,请添加一个利用内置文件复制功能的新方法:
public async Task FileCopyAsync(string sourceFileName, string destinationFileName, int bufferSize = 0x1000, CancellationToken cancellationToken = default(CancellationToken)) { using (var sourceFile = File.OpenRead(sourceFileName)) { using (var destinationFile = File.OpenWrite(destinationFileName)) { await sourceFile.CopyToAsync(destinationFile, bufferSize, cancellationToken); } } }- 重写
BackupDB方法的整体,使用新创建的方法:
public async Task BackupDB(string backupname) { string source = Environment.CurrentDirectory + "\\OnlineBank.db"; string destination = Environment.CurrentDirectory + "\\backups\\" + backupname; await FileCopyAsync(source, destination); } ```* - 打开
*我们重构了BackUpDB方法,使用FileCopyAsync方法将代码限制为仅执行文件复制任务,从而防止执行不需要的 shell 命令。
它是如何工作的…
在我们的示例解决方案中,允许管理员提供一个名称来创建数据库备份。BackUpDB方法接受string类型的用户控制输入参数。输入字符串用于形成一个命令,该命令将启动一个命令 shell,将文件从源复制到目标。
添加的输入字符串应具有目标文件名,但可以对其进行操作,以包含不仅仅是参数值的命令。如果没有验证或清理,这可能会导致应用在 web 应用的标识和授权下执行不需要的 shell 命令。
还有更多…
停止 OS 命令注入的一个选项是通过白名单技术实现适当的验证。这项技术可以通过使用正则表达式来实现(参见第一章、安全编码基础中的输入验证配方):
-
添加对
System.Text.RegularExpressions命名空间的引用:using System.Text.RegularExpressions; -
然后,使用
RegEx类及其IsMatch方法根据模式验证输入,只接受有效字符:public async Task BackupDB(string backupname) { var regex = new Regex(@"^[a-zA-Z0-9]+$"); if (!regex.IsMatch(backupname)) return; using (Process p = new Process()) { string source = Environment.CurrentDirectory + "\\OnlineBank.db"; string destination = Environment.CurrentDirectory + "\\backups\\" + backupname; p.StartInfo.Arguments = " /c copy " + source + " " + destination; p.StartInfo.FileName = "cmd"; p.StartInfo.CreateNoWindow = true; // code removed for brevity
我们现在使用IsMatch方法添加了白名单验证。IsMatch方法防止在后续代码行中处理非字母数字字符和输入,降低了命令注入的风险。
固定注入
轻型目录访问协议(LDAP)是一种标准协议,用于访问目录服务,如 Microsoft 的 Active Directory 和 Apache Directory。Web 应用使用 LDAP 搜索目录服务器以获取用户和组信息,这也是身份验证的一种手段。由于 LDAP 查询语言及其过滤器,可以将数据从 web 应用检索到 LDAP 目录服务器。开发人员编写代码来组合这些查询。与任何其他动态查询构造一样,当连接的用户控制输入未经验证或消毒时,此方法可以打开代码进行注入,特别是LDAP 注入。
在此配方中,我们将识别代码中的LDAP 注入漏洞,并修复安全漏洞。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter02\ldap-injection\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看一下这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令来构建示例应用,以确认没有编译错误:dotnet build- 打开
Services/LdapDirectoryService.cs文件,在Search(string userName)方法
public User Search(string userName) { using (DirectoryEntry entry = new DirectoryEntry(config.Path)) { entry.AuthenticationType = AuthenticationTypes.Anonymous; using (DirectorySearcher searcher = new DirectorySearcher(entry)) { searcher.Filter = "(&(" + UserNameAttribute + "=" + userName + "))"; searcher.PropertiesToLoad.Add (EmailAttribute); searcher.PropertiesToLoad.Add (UserNameAttribute); var result = searcher.FindOne(); // code removed for brevity中定位代码的易受攻击部分* 要修复 LDAP 注入漏洞,请重构代码以包含
userName参数public User Search(string userName) { if (Regex.IsMatch(userName, "^[a-zA-Z][a-zA-Z0- 9]*$")){ using (DirectoryEntry entry = new DirectoryEntry(config.Path)) { entry.AuthenticationType = AuthenticationTypes.Anonymous; using (DirectorySearcher searcher = new DirectorySearcher(entry)) { searcher.Filter = "(&(" + UserNameAttribute + "=" + userName + "))"; searcher.PropertiesToLoad.Add (EmailAttribute); searcher.PropertiesToLoad.Add (UserNameAttribute); var result = searcher.FindOne(); // code removed for brevity的*白名单验证**
- 打开
*通过使用正则表达式重用白名单技术,我们再次使用IsMatch方法来确定模式是否与输入匹配。如果输入与正则表达式不匹配,则拒绝输入。
它是如何工作的…
在我们的示例解决方案中,我们有一个网页,允许管理员用户使用搜索栏搜索特定的用户帐户:

图 2.7–管理用户页面
输入用户 ID 并点击搜索按钮将向 LDAP 目录服务发送 LDAP 查询,以搜索具有确切用户 ID 的用户:

图 2.8–搜索用户结果
笔记
本书中没有提供设置 LDAP 目录服务的步骤。假设您希望在本地计算机上运行的工作目录服务器与示例解决方案一起工作。在这种情况下,我建议您安装ApacheDS并按照官方 Crafter CMS 文档中设置 LDAP 服务器以使用 Apache Directory Studio页面进行开发/测试的步骤进行操作 https://docs.craftercms.org/en/3.1/developers/cook-books/how-tos/setting-up-an-ldap-server-for-dev.html 。
必要时更改appsettings.json中的Ldap条目:
"Ldap": {
"Path": "LDAP://localhost:10389/DC=example,DC=com",
"UserDomainName": "example"
},
在调用Search方法时,动态组合 LDAP 查询,并将过滤器与搜索文本框中输入的值连接起来:
searcher.Filter = "(&(" + UserNameAttribute + "=" + userName + "))";
userName参数未经过清理或验证,不良行为人可以通过注入可疑过滤器来利用此漏洞,该过滤器可以从 LDAP 目录服务器检索敏感信息。
为了降低这种风险,我们使用 Regex 的IsMatch方法添加了白名单验证方法。只有当userName中的任何字符为字母数字时,条件表达式才会等价于 true:
public User Search(string userName)
{
if (Regex.IsMatch(userName, "^[a-zA-Z][a-zA-Z0-9]*$")){
using (DirectoryEntry entry = new DirectoryEntry(config.Path))
{
// code removed for brevity
作为整体安全编码策略的一部分,实施白名单输入验证以检查用户控制的输入,从而保护 ASP.NET Core web 应用免受 LDAP 注入攻击。
固定注入
数据驱动的 ASP.NET Core web 应用可以使用 XML 数据库作为存储信息和记录的手段。这些数据类型是 XML 格式的,通过 XML 的节点导航的一种方式是通过XPath。
开发人员可能会错误地使用不受信任的数据动态构造 XPath 查询。这种忽略可能导致执行任意查询或从 XML 数据库检索敏感数据。
在此配方中,我们将修复代码中的XPath 注入漏洞。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter02\xpath-injection\before\OnlineBankingApp\的示例网上银行应用文件夹。
本例使用以下 XML 数据:
<?xml version="1.0" encoding="utf-8"?>
<knowledgebase>
<knowledge>
<topic lang="en">Types of Transfers</topic>
<description lang="en">
Make transfers from checking and savings to:
Checking and savings
Make transfers from line of credit to:
Checking and savings
</description>
<tags>transfers, transferring funds</tags>
<sensitivity>Public</sensitivity>
</knowledge>
<knowledge>
<topic lang="en">Expedited Withdrawals</topic>
<description lang="en">
Expedited withdrawals are available to our executive account holders.
You may reach out to Stanley Jobson at stanley.jobson@bank.com
</description>
<tags>withdrawals, expedited withdrawals</tags>
<sensitivity>Confidential</sensitivity>
</knowledge>
</knowledgebase>
怎么做…
让我们来看看这个食谱的步骤:
-
启动 Visual Studio 代码并通过键入以下命令打开启动练习文件夹:
code . -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令来构建示例应用,以确认没有编译错误:dotnet build- Open the
Services/KnowledgebaseService.csfile and locate the vulnerable part of the code in theSearchmethod:
public List<Knowledge> Search(string input) { List<Knowledge> searchResult = new List<Knowledge>(); var webRoot = _env.WebRootPath; var file = System.IO.Path.Combine(webRoot, "Knowledgebase.xml"); XmlDocument XmlDoc = new XmlDocument(); XmlDoc.Load(file); XPathNavigator nav = XmlDoc.CreateNavigator(); XPathExpression expr = nav.Compile(@"//knowledge[tags[contains(text() ,'" + input + "')] and sensitivity/text() ='Public']"); var matchedNodes = nav.Select(expr); // code removed for brevityXPath 表达式是通过连接用户控制的输入动态创建的。在没有对
input参数进行任何验证或清理的情况下,恶意参与者可以通过注入恶意字符串来操纵 XPath 查询,从而改变整个表达式的意图。- 为了修复这个安全缺陷,让我们重构代码并基于白名单技术实现输入净化。首先,添加对
System和System.Linq名称空间的引用:
using System; using System.Linq;- 在
KnowledgebaseService类中添加一个新方法,并将其命名为Sanitize:
private string Sanitize(string input) { if (string.IsNullOrEmpty(input)) { throw new ArgumentNullException("input", "input cannot be null"); } HashSet<char> whitelist = new HashSet<char> (@"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz "); return string.Concat(input.Where(i => whitelist.Contains(i))); ; }- 调用新的
Sanitize方法,将input参数作为参数传递给它。将结果分配给sanitizedInput变量:
public List<Knowledge> Search(string input) { string sanitizedInput = Sanitize(input); List<Knowledge> searchResult = new List<Knowledge>(); var webRoot = _env.WebRootPath; var file = System.IO.Path.Combine(webRoot, "Knowledgebase.xml"); XmlDocument XmlDoc = new XmlDocument(); XmlDoc.Load(file); XPathNavigator nav = XmlDoc.CreateNavigator(); XPathExpression expr = nav.Compile(@"//knowledge[tags[contains(text() ,'" + sanitizedInput + "')] and sensitivity/text()='Public']"); // code removed for brevity ```* - Open the
*自定义Sanitize方法现在将删除输入字符串中不必要且可能危险的字符。现在,输出被传递到一个sanitizedInput变量中,使得 XPath 表达式不受攻击。
它是如何工作的…
正如我们在第 1 章安全编码基础中所了解到的,在输入净化部分中,输入净化是一种防御技术,可用于移除用户提供输入中的可疑字符。这种方法将防止应用处理注入查询的不需要的XPath。
我们创造了新的Sanitize方法,作为我们的消毒剂。此方法中包含一个已定义字符的白名单,并调用 Lambda 来删除userName中被拒绝的字符:
HashSet<char> whitelist = new HashSet<char>(@"1234567890ABCDEFGHI JKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ");
return string.Concat(input.Where(i => whitelist.Contains(i))); ;
搜索带有不可接受字符的帮助文章不会引发异常,我们的示例网上银行 web 应用也不会处理该字符串:

图 2.9–搜索知识库
还有更多…
另一种修复方法是参数化 XPath 查询。我们将定义一个变量,作为参数的占位符。此技术允许将数据与代码分离:
XmlDocument XmlDoc = new XmlDocument();
XmlDoc.Load(file);
XPathNavigator nav = XmlDoc.CreateNavigator();
XPathExpression expr = nav.Compile(@"//knowledge[tags[contains(text(),$input)] and sensitivity/text()='Public']");
XsltArgumentList varList = new XsltArgumentList();
varList.AddParam("input", string.Empty, input);
CustomContext context = new CustomContext(new NameTable(), varList);
expr.SetContext(context);
var matchedNodes = nav.Select(expr);
foreach (XPathNavigator node in matchedNodes)
{
searchResult.Add(new Knowledge() {Topic = node.SelectSingleNode(nav.Compile("topic")) .Value,Description = node.SelectSingleNode (nav.Compile("description")).Value});
}
在前面的代码中,XPath 表达式被修改,$input变量现在是先前连接的input值的占位符。我们还使用了XsltArgumentList对象创建了一个参数列表,以在传递到XpathExpression表达式的自定义上下文之前包含输入。通过这种方式,XPath 查询被参数化,并在执行时防止恶意注入。
笔记
此缓解措施需要创建一个从XsltContext派生的用户定义的自定义上下文类。要使 XPath 参数化成为可能,还需要其他类。示例解决方案中包括类文件,即:;Services\XPathExtensionFunctions.cs、Services\XPathExtensionVariable.cs和Services\CustomContext.cs。这些课程的完整指南和来源也可在线访问.NET 官方文档:https://docs.microsoft.com/en-us/dotnet/standard/data/xml/user-defined-functions-and-variables 。********
三、认证中断
在 ASP.NET web 应用中,验证和确认身份可能是最关键的安全要求——身份验证。未能实现强身份验证允许黑客暴露此漏洞并利用它获得禁止访问。
弱密码策略、缺少暴力攻击预防机制、弱哈希密码和长活动会话是这些身份验证缺陷的几个根本原因。正确的凭证管理和会话配置是防止代码中出现这些漏洞的关键。本章将教我们如何解决这些问题。
在本章中,我们将介绍以下配方:
- 修复过度身份验证尝试的不正确限制
- 修复保护不足的凭据
- 修复用户枚举
- 修复弱密码要求
- 修复会话到期时间不足的问题
在本章结束时,您将学习如何实施验证码以防止暴力攻击,使用健壮的哈希算法更好地保护您的密码,发送通用消息以避免不必要的信息被暴露,以及配置会话以使其在可接受的时间段内过期。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。您可以在找到本章的代码 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter03 。
修复过度身份验证尝试的不正确限制
对手总是试图访问系统并击败其身份验证机制。最普遍的做法是使用从非法来源收集的泄露凭据,或者简单到拥有一个可以在 web 上找到的常用密码列表。可以使用特制的脚本或工具自动执行这些攻击。
ASP.NET Core web 应用必须通过实施防御措施来抵御这种攻击。这有助于拒绝过多的身份验证尝试和编写安全代码。
在此配方中,我们将确定代码中漏洞的根本原因,并通过启用锁定功能来缓解问题。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter03\improper-auth\before\OnlineBankingApp的示例网上银行应用文件夹。
测试对过度身份验证尝试的限制
遵循以下步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000。* The browser will display the Log in page:
![Figure 3.1 – Log in page]()
图 3.1–登录页面
- 尝试以输入
stanley.s.jobson@lobortis.ca作为电子邮件地址,输入password123作为密码登录。* You will see an error stating Invalid login attempt:
![Figure 3.2 – Invalid login attempt]()
图 3.2–无效登录尝试
- 重复步骤 5五次以上,尝试调用账户锁定。*
- 打开浏览器并转到
*在多次失败的登录尝试后,请注意该帐户未被锁定。此缺少的锁定功能使 ASP.NET Core web 应用容易受到暴力攻击。
怎么做…
让我们来看看这个食谱的步骤:
-
使用前面步骤中打开的同一样本解决方案,在终端中键入以下命令来构建样本应用,以确认没有编译错误:
dotnet build -
打开
\Chapter03\improper-auth\before\OnlineBankingApp\Areas\Identity\Pages\Account\Login.cshtml.cs文件,找到调用PasswordSignInAsync方法的代码行:public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationScheme sAsync()).ToList(); if (ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync(Input. Email, Input.Password, Input.RememberMe,lockoutOnFailure: false); if (result.Succeeded) { _logger.LogInformation("User logged in."); return LocalRedirect(returnUrl); } -
Change the last parameter in the
PasswordSignInAsyncmethod fromfalsetotrue:var result = await _signInManager.PasswordSignInAsync(Input.Email, Input. Password, Input.RememberMe, lockoutOnFailure: true);将
lockoutOnFailure参数从false更改为true将在多次密码登录失败时触发用户帐户锁定。 -
Next, configure the
Identityservice by setting theDefaultLockoutTimeSpanandMaxFailedAttemptsproperties:services.AddIdentity<Customer,IdentityRole>(options => { options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 3; })设置锁定策略将配置示例网上银行 web 应用的锁定行为。
DefaultLockoutTimeSpan设置用户被锁定的时间,以分钟为单位。此设置的默认值为5分钟。在锁定用户之前,默认密码尝试失败次数为
5。通过分配IdentityOptions的MaxFailedAccessAttempts属性可以覆盖默认值,该属性可以在AddIdentity方法中配置,并从ConfigureServices方法调用。MaxFailedAccessAttempts设置为在三次登录尝试失败后锁定用户。这些属性的值应根据组织的锁定策略指定值。
它是如何工作的…
PasswordSignInAsync是我们用来使用Input.Email和Input.Password值登录Customer的方法调用。它需要一个lockoutOnFailure参数,该参数指示如果登录失败了指定次数的尝试,是否应从帐户中锁定Customer。
lockoutOnFailure参数设置为true以启用登录失败时的锁定。这将防止恶意参与者进行自动和过度的身份验证重试。失败尝试的次数可以使用LockoutOptions类的MaxFailedAccessAttempts属性定义。
提示
确保记录失败的登录尝试和帐户锁定。应记录并监控这些类型的事件,以防潜在的暴力攻击。您将在第 11 章中了解更多关于正确的日志记录和监控技术,日志记录和监控不足。
还有更多…
另一个有助于防止暴力攻击的安全功能是实施验证码。验证码是一种挑战响应测试,有助于确定是人还是计算机执行了该操作。这种类型的测试可以帮助检测滥用和自动登录尝试。
有多种验证码系统可供网络开发者使用,其中谷歌的reCAPTCHA最受欢迎。要在 ASP.NET Core web 应用中使用 Google 的reCAPTCHA,请执行以下步骤:
-
在谷歌开发者网站(上注册reCAPTCHA服务 https://developers.google.com/recaptcha/intro 获取 API 密钥对。此 API 密钥对将用于将 reCAPTCHA 集成到我们的示例解决方案中。
-
将 localhost 注册为网站时,请选中 reCAPTCHA v2我不是机器人复选框。此配方基于 reCAPTCHA 系统的第 2 版。
-
在终端中键入以下命令以安装Google reCAPTCHA ASP.NET Core 3库:
dotnet add package reCAPTCHA.AspNetCore -
The Google reCAPTCHA ASP.NET Core 3 library is a third-party and open source NuGet package that helps ASP.NET Core web developers integrate the Google reCAPTCHA system easily.
警告
使用任何第三方库都有风险。在将包集成到 ASP.NET Core web 应用之前,您可能需要查看包的代码。确保您使用的是最新且稳定的版本。
-
在
appsettings.json文件{ "RecaptchaSettings": { "SecretKey": "secret key", "SiteKey": "site key" }, "https_port": 443,中为 reCAPTCHA 设置创建新条目
-
打开
\Chapter03\improper-auth\before\captcha\OnlineBankingApp\Startup.cs文件并添加对 Google ASP.NET reCAPTCHA 库的引用:using reCAPTCHA.AspNetCore; -
在
ConfigureServices方法services.AddRecaptcha(Configuration.GetSection("RecaptchaSettings"));中注册 reCAPTCHA 服务
-
选择要在其中启用 Google reCAPTCHA 的 Razor 页面。通常,这被放置在预期会出现身份验证或注册滥用的页面上。打开
\Chapter03\improper-auth\before\captcha\OnlineBankingApp\Areas\Identity\Pages\Account\Login.cshtml文件并添加对 reCAPTCHA 命名空间的引用:@page @using reCAPTCHA.AspNetCore @using reCAPTCHA.AspNetCore.Versions; -
包括对
Microsoft.Extensions.Options的引用。对这个Microsoft.Extensions.Options名称空间的引用提供了对实现Options模式的类的访问,包括与 reCAPTCHA 相关的配置设置:@using Microsoft.Extensions.Options; -
在
submit按钮下方添加 Recaptcha HTML 帮助程序,以便 Razor 页面呈现 Recaptcha 并显示质询:
```cs
<div class="form-group">
<button type="submit" class="btn btn-primary"> Log in</button>
</div>
<div class="form-group">
@(Html.Recaptcha<RecaptchaV2Checkbox>(Recaptcha Settings?.Value))
</div>
```
- 打开相应的页面模型类“
\Chapter03\improper-auth\before\captcha\OnlineBankingApp\Areas\Identity\Pages\Account\Login.cshtml.cs文件并添加对 reCAPTCHA 命名空间的引用:
```cs
using reCAPTCHA.AspNetCore;
```
- 使用依赖项注入,声明一个私有的
readonly对象,该对象将保存IRecaptchaService的实例。向LoginModel构造函数添加一个新参数,该构造函数将在类
```cs
private readonly IRecaptchaService _recaptcha;
public LoginModel(SignInManager<Customer> signInManager,
ILogger<LoginModel> logger,
UserManager<Customer> userManager,
IRecaptchaService recaptcha)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_recaptcha = recaptcha;
}
```
中公开注入的服务
- 在
OnPostAsync方法中,在发生账户登录验证的地方调用,并添加以下突出显示的代码行以验证 reCAPTCHA 响应:
```cs
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
var recaptcha = await _recaptcha.Validate(this.HttpContext.Request);
if (!recaptcha.success) ModelState.AddModelError("Recaptcha", "Error Validating Captcha");
```
- 来自 reCAPTCHA 服务的
Validate方法调用将获取当前 HTTP 上下文,并检查用户的 CAPTCHA 响应是否有效。 - 重复测试过度验证尝试的限制部分的步骤 1 至 7并在尝试登录时查看执行验证码测试的结果:

图 3.3–带有 reCAPTCHA 的登录页面
用 reCAPTCHA 服务补充 ASP.NET Core web 应用现在可以防止自动攻击和暴力强迫。
修复保护不足的凭据
密码破坏者和破解者现在比以往任何时候都更强大,拥有先进的硬件和无尽的计算资源。仅仅对密码进行散列已经不够了,现在关键是选择正确的散列函数,以保护凭据在发生数据泄露时不被暴露。
在这个配方中,我们将修改实现弱散列函数的代码,并将其替换为 BCrypt。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter03\insufficient-protected-creds\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在终端中键入以下命令来构建示例应用,以确认没有编译错误:
dotnet build -
Open the
\Chapter03\insufficient-protected-creds\before\OnlineBankingApp\Areas\Identity\PasswordHasher.csfile. ThePasswordHasherclass is derived fromIPasswordHasher, which lets you define your own custom hashing mechanism for your ASP.NET Core web application. Notice, however, that the hashing algorithm being used in theHashPasswordmethod is MD5, which is a known weak hashing algorithm:public string HashPassword(Customer customer, string password) { using (var md5 = new MD5CryptoServiceProvider()) { var hashedBytes = md5.ComputeHash(System.Text .Encoding.UTF8.GetBytes(password)); var hashedPassword = BitConverter.ToString(hashedBytes) .Replace("-", "").ToLower(); return hashedPassword; } }笔记
开发人员定制散列过程并选择较弱算法的原因有很多——从遗留应用迁移和向后兼容性就是其中之一。将您的密码迁移到一个更健壮的算法中仍然是明智的,也是值得的。
-
在 Visual Studio 代码终端中,键入以下命令以安装
Bcrypt.NetNuGet 软件包:dotnet add package BCrypt.Net-Next -
Bcrypt.Netis a .NET library implementation of the Bcrypt hashing function based on the Blowfish cipher. The BCrypt hashing function implements a strong security measure as it adds salt to the hashing process. Bcrypt.Net lets developers define their own salt in the hash, but it is advisable to just let the library generate its own salt.注:
有关BCrypt哈希算法的更多信息和细节,请参见 USENIX 网站上由 BCrypt 算法设计者Niels Provos 和 David Mazieres撰写的名为A Future Adaptive Password Scheme的官方出版物:
https://www.usenix.org/legacy/events/usenix99/provos/provos_html/node1.html 。
-
添加对 Bcrypt.NET 命名空间的引用:
using BC = BCrypt.Net.BCrypt; -
使用
AddSingleton方法services.AddSingleton<IEmailSender, EmailSender>(); services.Configure<AuthMessageSenderOptions>(Configuration); services.AddSingleton<IPasswordHasher<Customer>, PasswordHasher>();注册我们的定制
IPasswordHasher服务 -
将中的
HashPassword和VerifyHashedPassword方法替换为以下代码:public class PasswordHasher : IPasswordHasher<Customer> { public string HashPassword(Customer customer, string password) { return BC.HashPassword(password); } public PasswordVerificationResult VerifyHashedPassword(Customer customer, string hashedPassword, string password) { if (BC.Verify(password, hashedPassword)) return PasswordVerificationResult.Success; else return PasswordVerificationResult.Failed; } }
在这里,我们分别使用BCrypt库的HashPassword和Verify方法对密码进行散列,并验证散列。
它是如何工作的…
我们的示例应用通过创建一个PasswordHasher类并从IPasswordHasher接口继承来定制散列算法实现,以修改密码的散列方式。已知MD5散列算法是一种弱密码。我们可以通过实现bcrypt算法来替换这个易受攻击的哈希函数。
我们通过BCrypt.Net-NextNuGet 包安装了 BCrypt.NET 库实现,添加了对BCrypt.Net.BCrypt命名空间的引用,并重写了整个HashPassword函数来调用 BCrypt.NET 的HashPassword方法。
注:
大多数安全专家更喜欢Argon2,这是 2015 年 7 月推出的一种较新的哈希函数,具有计算密集型硬内存功能,使其能够抵御基于硬件的攻击。Isopoh.Cryptography.Argon2是一个开源项目,支持 Argon2 哈希函数实现,ASP.NET Core 开发人员可以使用该库,如果他们喜欢使用这种哈希方法。您可以按照安装.NET Core Argon2 库的说明操作:https://github.com/mheyman/Isopoh.Cryptography.Argon2 。
另一方面,Bcrypt仍然是一个不错的选择,因为它的过程非常耗时,很难打破;但是,它可能容易出现基于 GPU 的开裂。
固定用户枚举
每一条信息对恶意行为者都至关重要。知道用户是否存在于 web 应用中,使攻击者能够利用此漏洞执行更具破坏性和更成功的攻击。通过在身份验证失败期间显示常规消息来不提供此信息,从而保护 ASP.NET Core web 应用。
在此配方中,我们将更改显示非常规错误消息的代码,以防止用户枚举攻击。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter03\user-emumeration\before\OnlineBankingApp的示例网上银行应用文件夹。
测试用户枚举
遵循以下步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000。* 浏览器将显示登录页面(参见图 3.1。* 以ginger.knowles@bank.com为用户名,以password123为密码登录。* 注意错误消息客户不存在:*
- 打开浏览器并转到
*
图 3.4–用户枚举
此错误消息向攻击者提供系统中不存在特定电子邮件地址的信息。然后,攻击者可以使用此信息收集具有帐户或记录的电子邮件地址。
怎么做…
让我们看看这个食谱的步骤:
-
在终端中键入以下命令来构建示例应用,以确认没有编译错误:
dotnet build -
打开
\Chapter03\user-enumeration\before\OnlineBankingApp\Areas\Identity\Pages\Account\Login.cshtml.cs文件,找到抛出Customer does not exist.错误消息的OnPostAsync方法调用中的代码行:if (result.IsLockedOut) { _logger.LogWarning("Customer account locked out."); return RedirectToPage("./Lockout"); } else { var user = await _userManager.FindByEmailAsync(Input.Email); if (user == null) { ModelState.AddModelError(string.Empty, "Customer does not exist."); return Page(); } } -
修改代码,使其返回一般消息;也就是说,
Invalid login attempt.:if (result.IsLockedOut) { _logger.LogWarning("Customer account locked out."); return RedirectToPage("./Lockout"); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); }
更改登录尝试失败时显示的错误消息将防止用户枚举攻击。
它是如何工作的…
对手可以通过分析应用的行为来收集信息,尤其是 ASP.NET Core web 应用向其用户显示的消息。消息“客户不存在”表示数据库中不存在电子邮件地址(在本例中用作用户名)。然后,这个恶意参与者可以列出一个有效用户名和电子邮件地址列表,用于其他恶意活动。
在这里,我们用一个通用的方法替换了对AddModel方法调用中的错误消息,以避免这种枚举。我们还阻止了现有客户帐户和不存在客户帐户之间的确定。
修复弱密码要求
用户凭证或密码的复杂性决定了字典攻击成功的可能性。如果密码不够复杂,对手只需几分钟就可以猜到用于使用自动化在 ASP.NET Core web 应用中进行身份验证的凭据。
在此配方中,我们将更改 ASP.NET Identity 的密码属性,以实现更强大的密码策略。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter03\weak-password-policy\before\OnlineBankingApp的示例网上银行应用文件夹。
测试弱密码策略
遵循以下步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
http://localhost:5000/Identity/Account/Register。* The browser will display the registration page:
![Figure 3.5 – Registration page]()
图 3.5–注册页面
- 填写表格并输入
password123作为密码。*
- 打开浏览器并转到
*请注意,web 应用创建了该帐户,尽管密码很弱。
怎么做…
让我们来看看这个食谱的步骤:
-
在终端中键入以下命令来构建示例应用,以确认没有编译错误:
dotnet build -
打开
\Chapter03\weak-password-policy\before\OnlineBankingApp\Startup.cs文件,进入ConfigureServices方法,检查IdentityOptions:public void ConfigureServices(IServiceCollection services) { services.Configure<IdentityOptions>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; });的
Password属性 -
ASP.NET Core 的
IdentityOptions配置为具有弱密码策略,因此覆盖了RequireLowercase、RequireNonAlphanumeric和RequireUppercase属性的默认安全值。 -
将
RequireLowercase、RequireNonAlphanumeric和RequireUppercase属性的值更改为true,以实施更强大的密码策略:public void ConfigureServices(IServiceCollection services) { services.Configure<IdentityOptions>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonAlphanumeric =true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; }); -
重复测试弱密码策略部分的1 至5 步,查看是否可以在使用
pasword123作为密码时创建用户帐户。
它是如何工作的…
不需要小写、字母数字和大写字符不再是可接受的密码策略。需要一个可靠的密码策略来阻止成功的基于凭证的暴力攻击,因为这有助于阻止我们的客户帐户被破坏。在前面的步骤中,我们通过将 ASP.NET Core 身份服务的RequireLowercase、RequireNonAlphanumeric和RequireUppercase属性设置为true来启用强密码策略。
您可以在 ASP.NET Core web 应用中实现的另一层防御是多因素身份验证(MFA。ASP.NET Core 中的 MFA 超出了本书的范围,但要了解更多有关 MFA 的信息,请查看 ASP.NET Core 中的多因素认证主题,该主题位于的 ASP.NET 官方在线文档中 https://docs.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-5.0。
修复会话到期不足
通常,web 应用创建会话,以维护用户在多个请求之间与 web 服务器的交互。这些会话绑定用户的身份并支持跟踪经过身份验证的用户。ASP.NET Core web 应用必须将经过身份验证的用户会话的长度保持在最小值。这有助于避免在发生基于会话的攻击时,给坏参与者带来很大机会的风险。
在此配方中,我们将缩短会话的有效性,以降低基于会话的攻击的风险。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter03\improper-session\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在终端中键入以下命令来构建示例应用,以确认没有编译错误:
dotnet build -
打开
\Chapter03\weak-password-policy\before\OnlineBankingApp\Startup.cs文件,找到设置应用 cookie 的代码。注意ExpireTimeSpan,它配置 cookie 保持有效的时间长度。设置为 24 小时:services.ConfigureApplicationCookie(options => { options.LoginPath = $"/Identity/Account/Login"; options.LogoutPath = $"/Identity/Account/Logout"; options.AccessDeniedPath = $"/Identity/Account/AccessDenied"; options.ExpireTimeSpan = TimeSpan.FromHours(24); }); -
为
ExpireTimeSpan属性分配较短的超时时间:services.ConfigureApplicationCookie(options => { options.LoginPath = $"/Identity/Account/Login"; options.LogoutPath = $"/Identity/Account/Logout"; options.AccessDeniedPath = $"/Identity/Account/AccessDenied"; options.ExpireTimeSpan = TimeSpan.FromMinutes(15); });
ExpireTimeSpan的值现在变短了,这使得对手很难利用一个开放且有效的会话。
它是如何工作的…
ASP.NET Core web 应用的 cookie 设置通过调用ConfigureApplicationCookie方法进行设置。此方法接受CookieAuthenticationOptions,包括确定 cookie 行为的属性。指定会话 cookie 有效性的ExpireTimeSpan属性已设置为15分钟,而不是 24 小时。
注:
由于您公司的业务性质,您的 ASP.NET Core web 应用可能受您的组织或法规设置的信息安全标准的约束。例如,对于支付卡行业,您必须遵守支付卡行业数据安全标准(PCI-DSS)第 6.5.10 节关于中断身份验证和会话管理的要求。
要了解更多关于 PCI-DSS 的信息,请阅读 PCI-DSS 官方文档:https://www.pcisecuritystandards.org/documents/PCI_DSS_v3-2-1.pdf?agreement=true &时间=1612424525744。***
四、敏感数据暴露
传输中和静止时的数据保护至关重要。确保使用强大的传输协议和 web 安全指令可以防止数据在传输过程中受到破坏,并防止意外的敏感数据泄露。在代码中使用最新的传输层安全(TLS协议版本)有助于缓解 TLS 较低版本的实现缺陷带来的这些漏洞,使中间人攻击如贵宾犬、LogJam等怪胎很难成功。
本章将帮助您确定代码中是否存在充分保护传输中和静止数据的安全要求。您还将了解可以在 ASP.NET Core web 应用中实现哪些其他 web 安全机制来保护您免受不必要的数据泄漏。
在本章中,我们将介绍以下配方:
- 修复传输中数据保护不足的问题
- 修复丢失的HTTP 严格传输安全(HSTS】头
- 修复弱协议
- 修复硬编码加密密钥
- 禁用关键网页的缓存
这些方法将教您如何在 ASP.NET Core web 应用中实现 HTTPS、启用 HST、确保应用了最新版本的 TLS,以及如何保护加密密钥。本章还将讨论发送缓存控制指令以禁用包含敏感数据的页面上的缓存。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。本书配方中的代码示例主要在 ASP.NET Core Razor 页面中介绍。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter04 。
修复传输中数据保护不足
TLS 是一种网络通信协议,用于在 web 上保护数据并通过加密实现隐私。当浏览器和 web 服务器之间传输的敏感数据未加密或可能被截获时,此安全协议的缺失或有缺陷的实现会给 ASP.NET web 应用带来巨大的风险。启用 TLS 是充分加密传输中数据的第一步。本章中的后续配方将提供更多保护。
不在 ASP.NET Core web 应用中启用 TLS 会使客户端和服务器之间传输的机密数据面临风险。您必须确保HTTPS已配置为最佳保护。
在本教程中,我们将学习如何正确降低缺少安全协议实现和 HTTPS 支持的风险。
准备好了吗
为了完成本章中的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆 ASP.NET Secure Codeing Cookbook 存储库下载示例网上银行应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter04\insufficient-transport-protection\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
怎么做…
让我们来看看这个配方的步骤:
-
在“开始练习”文件夹中,通过键入以下命令来启动 Visual Studio 代码:
code . -
Open the
Startup.csclass file and add the following code before the closing braces of theConfigureServicesmethod:if (Environment.IsDevelopment()) { services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect; options.HttpsPort = 5001; }); } else { services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect; options.HttpsPort = 443; }); }我们添加了一个条件语句,用于检查当前环境是否正在开发或生产中运行。此检查将帮助我们确定是否必须使用标准 HTTPS 端口
443执行临时重定向或永久重定向。 -
在
Configure方法app.UseHttpsRedirection();中对
UseStaticFiles的调用之前添加对UseHttpsRedirection方法的调用 -
Open the
launchsettings.jsonfile and change the values inapplicationUrlandenvironmentVariables, as shown in the following highlighted code:"OnlineBankingApp": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "http://localhost:5000;https://localhost:5001 }launchsettings.json文件是一个配置文件,允许您在开发过程中在多个环境中工作。部署到生产环境时不包括此文件。如果您有IISExpress的设置,您可能还需要更改ASPNETCORE_URLS环境变量和applicationURL。 -
在菜单中导航至终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器,进入
http://localhost:5000:*
- 打开浏览器,进入
*
图 4.1–启用 HTTPS
请注意,样本溶液被重定向到https://localhost:5001。
笔记
Web API 超出了本书的范围,但需要注意的是,必须在 API 中禁用 HTTP 重定向。Web API 还应拒绝 HTTP中的请求,并返回HTTP 400状态码。
它是如何工作的…
在Startup类中,我们调用AddHttpsRedirection方法向服务集合注册UseHttpsRedirection。我们通过设置两个属性来配置中间件选项:RedirectStatusCode和HttpsPort。默认情况下,RedirectStatusCode为Status307TemporaryRedirect,但在生产环境中应更改为Status308PermanentRedirect,以防止用户代理(也称为浏览器)将 HTTP 方法从POST更改为GET。我们还必须指定443的 HTTPS 标准端口:
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});
提示
对AddHttpsRedirection的调用是可选的,除非您需要不同的重定向状态代码或 443 以外的其他端口。
为了将 HTTP 请求重定向到 HTTPS,我们必须在Configure中添加 HTTPS 中间件,并调用UseHttpsRedirection方法。
笔记
安全套接字层又称 SSL,是 TLS 的前身。其他参考文献将 SSL 和 TLS 描述为一体,但本书中使用了 TLS 来建议最新版本的加密协议。
然而,TLS 支持种类繁多的密码套件,并且并非所有密码套件都是相同的。有些密码提供了更好的安全性,例如GCM 密码,这是理想的选择。有关密码的更多信息,请参阅 OWASP:的TLS 密码字符串备忘单https://cheatsheetseries.owasp.org/cheatsheets/TLS_Cipher_String_Cheat_Sheet.html 。
修复缺失的 HSTS 头
HTTP 严格传输安全或HSTS是另一种帮助防止中间人攻击的 web 应用安全机制。它允许 web 服务器发送一个特殊的 HTTP 响应头,通知支持浏览器后续的通信和数据传输只能通过 HTTPS 完成;否则,将不允许后续连接。
未能选择加入 HSTS 作为附加安全策略并不能消除敏感数据拦截的威胁。用 HST 补充 HTTPS 将阻止用户暴露于未加密通道的风险。
这个方法将教会我们如何启用示例 ASP.NET Core web 应用中缺失的 HST,以强制客户端完全通过 HTTPS 进行通信。
准备好了吗
我们将使用上一个配方中使用的在线银行应用。使用 Visual Studio 代码,打开位于\Chapter04\missing-hsts\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来修复丢失的 HSTS 标头。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open the
Startup.csclass file and add the following code before theAddHttpsRedirectioncall of theConfigureServicesmethod:services.AddHsts(options => { options.ExcludedHosts.Clear(); options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(60); }); if (Environment.IsDevelopment()) { services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect; options.HttpsPort = 5001; }); } else { services.AddHttpsRedirection(options => { options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect; options.HttpsPort = 443; }); }提示
调用
ExcludedHostsProperty的Clear方法用于在本地和开发中测试 HST。此方法阻止localhost被排除,因为它是环回主机。此代码必须在生产环境中删除。您还可以为本地主机和/或开发环境主机添加排除选项作为替代:
options.ExcludedHosts.Add("localhost");当 HST 设置不正确时,在本地测试 HST 可能会导致一些意外错误。遇到错误时,必须清除 HSTS 缓存。
-
添加对
UseHsts方法if (webHostEnv.IsDevelopment()) { appBuilder.UseDeveloperExceptionPage(); } else { appBuilder.UseExceptionHandler("/Error"); appBuilder.UseHsts(); }的调用
-
为了使
IsDevelopment方法返回false并使我们能够在本地测试 HST,我们需要临时更改环境变量。打开`Chapter04\missing-hsts\before\OnlineBankingApp\Properties\launchSettings.json并找到ASPNETCORE_ENVIRONMENT节点。将数值从Development更改为Staging`:"environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Staging", "ASPNETCORE_URLS": "http://localhost:5000;https://localhost:5001" } -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 按F12打开浏览器的开发者工具。* 进入网络选项卡,监控即将到来的流量。* Open a browser and go to
http://localhost:5000.
您的浏览器将自动将您重定向到
https://localhost:5001。- 在开发者工具窗格的网络选项卡中,选择已发送到本地主机的第二组 HTTP 流量,并检查 HTTP 响应头:*
- 按F12打开浏览器的开发者工具。* 进入网络选项卡,监控即将到来的流量。* Open a browser and go to
*
图 4.2–严格传输安全 HTTP 头
响应中包含一个新的 HTTP 头。严格传输安全 HTTP 头在 ASP.NET Core web 应用中启用 HST。
它是如何工作的…
要在我们的 ASP.NET Core web 应用中启用 HSTS,我们需要通过调用AddHsts来添加 HSTS 服务。此处配置了 HSTS 服务的以下属性:
-
Preload:发送预加载标志作为严格传输头的一部分。它指示支持浏览器将 ASP.NET Core web 应用的域包含在预加载列表中,从而阻止用户通过 HTTP 访问。 -
IncludeSubdomains:另一个指令,允许子域启用 HST,而不仅仅是顶级域名。 -
MaxAge: A property that determines the max-age directive of the Strict-Transport-Security header. This dictates how long the browser will remember to send requests for over HTTPS in seconds.笔记
将
MaxAge属性值设置为 31536000 秒,可以保证HSTS头中的max age属性在至少一年期间被强制执行,还可以防止用户手动接受不受信任的 SSL 证书。
最后,对UseHsts方法的调用为 HST 添加了中间件。
还有更多…
在中使用严格传输安全头中的preload标志可能会给您的用户带来不良后果。当您在设置 HSTS 中间件时启用此属性时,请轻描淡写。例如,尝试通过 HTTP 再次浏览该站点:

图 4.3–启用 HST/通过 HTTP 浏览
请注意,浏览器阻止您使用示例 ASP.NET web 应用。
笔记
并非所有浏览器都支持 HSTS 标头。要查看支持 HST 的浏览器列表,请参阅 Mozilla 开发者官方网站上的严格传输安全浏览器兼容性表 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#browser_compatibility 。
修复弱协议
被称为 TLS 的加密协议经过多年的发展,最初是作为安全套接字层,大多数通常被称为SSL。这一点现在已被弃用,因此发现其后续产品的设计中存在漏洞。最新版本的传输层安全协议 TLS1.3 就是为了解决这些问题而创建的。
启用 HTTPS 和使用 TLS 不足以保护您的 ASP.NET Core web 应用免受意外数据泄露。对手可能利用 TLS 的弱版本进行攻击。要克服这一点,您必须使用最新和最好的加密密码和协议版本。
此配方将教您如何更改过时的 TLS 版本,并编写代码以利用 TLS 1.3 版本的协议。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter02\weak-protocol\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来修复弱协议。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
打开
Program.cs类文件。此类设置主机,默认情况下,使用Kestrel作为 web 服务器。CreateHostBuilder方法调用用于定位SslProtocols属性被分配了更低版本 TLS 的行。当前值表示使用的 TLS 版本为版本TLS 1.0:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(host => { host.UseKestrel(options => { options.ConfigureHttpsDefaults (https => { https.SslProtocols = SslProtocols.Tls; }); }); host.UseStartup<Startup>(); }); -
分配
SslProtocols属性,以及更强大的 TLS 版本:options.ConfigureHttpsDefaults(https => { https.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; }); -
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 按F12打开浏览器的开发者工具。* 进入网络选项卡,监控即将到来的流量。* 打开浏览器并转到
http://localhost:5000。您的浏览器将自动将您重定向到https://localhost:5001。* 转到开发者工具窗格的安全选项卡,查看安全连接详细信息:*
- 按F12打开浏览器的开发者工具。* 进入网络选项卡,监控即将到来的流量。* 打开浏览器并转到
*
图 4.4–安全连接详细信息
安全选项卡显示正在使用的 TLS 协议版本的详细信息,以及密码。
它是如何工作的…
当前版本的代码使用较低且易受攻击的 TLS 版本。SslProtocols分配了一个枚举值SslProtocols.Tls,相当于TLS 1.0。截至 2020 年 3 月,TLS 1.0 已被弃用。
*此示例 ASP.NET Core web 应用中使用的默认 web 服务器是Kestrel。在代码中,我们将 Kestrel 的设置配置为UseKestrel,将属性值作为选项传递。
其中一个属性是SslProtocols,我们应该使用SslProtocols.Tls12和SslProtocols.Tls13管道枚举值来指定只允许TLS 1.2和TLS 1.3使用。这些将被设置为我们的默认值。
固定硬编码加密密钥
加密密钥是整个加密生态系统的重要组成部分。此字符串对于加密和解密敏感数据至关重要。特别是,非对称密码算法具有私钥(作为公私密钥对交换的一部分),这意味着可以防止被窥探。如果这些密钥落入坏人手中或泄漏,攻击者将能够成功执行敏感数据解密。
笔记
应用密码学的主题是广泛而复杂的。本书在本章中简要介绍了弱加密算法,但要更全面地了解加密在.NET 中的实现方式,请阅读 Microsoft 官方在线文档中的.NET 加密模型 https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model 。
下面的方法将帮助我们发现代码中的安全漏洞,该漏洞会破坏这些加密私钥,并通过代码修复来修复风险。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter04\hard-coded-key\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来修复硬编码加密密钥。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open the
\Areas\Identity\Pages\Account\Register.cshtml.csfile and go to theOnPostAsyncmethod definition:public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationScheme sAsync()).ToList(); if (ModelState.IsValid) { var user = new Customer { FirstName = _cryptoService.Encrypt (Input.FirstName, key), MiddleName = _cryptoService.Encrypt (Input.MiddleName, key), LastName = _cryptoService.Encrypt (Input.LastName, key), DateOfBirth = Input.DateOfBirth, UserName = Input.Email, Email = Input.Email };笔记
完整的代码可以在本书的 GitHub 存储库中找到,网址为 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook\第四章。
-
注意对
_cryptoService.Encrypt方法的调用有第二个接受字符串的参数。此参数期望接收一个key,但正在传递的密钥是以代码形式硬编码的。key类字段被声明并分配了一个硬编码值BGw3UHkI4z:[AllowAnonymous] public class RegisterModel : PageModel { private readonly SignInManager<Customer> _signInManager; private readonly UserManager<Customer> _userManager; private readonly ILogger<RegisterModel> _logger; private readonly IEmailSender _emailSender; private readonly ICryptoService _cryptoService; private const string key = "BGw3UHkI4z"; -
我们可以通过从安全环境(如环境变量)检索密钥来解决此问题。环境变量是您的操作系统中用于各种应用和服务的变量:
FirstName = _cryptoService.Encrypt(Input.FirstName, Environment.GetEnvironmentVariable("securekey", EnvironmentVariableTarget.Machine)), MiddleName = _cryptoService.Encrypt(Input.MiddleName,Environment.GetEnvironmentVariable("securekey", EnvironmentVariableTarget.Machine)), LastName = _cryptoService.Encrypt(Input.LastName, Environment.GetEnvironmentVariable("securekey", EnvironmentVariableTarget.Machine)), -
我们可以使用
Environment.GetEnvironmentVariable方法检索securekey环境变量中存储的密钥的值。 -
Create the new
securekeyenvironment variable.如果您是 Windows 用户,请执行以下步骤:
a) 打开 Windows运行提示,输入
sysdm.cpl,然后按确定或进入:![Figure 4.5 – Windows Run prompt]()
图 4.5–Windows 运行提示
b) 点击高级选项卡,然后点击环境变量按钮:
![Figure 4.6 – Advanced System Properties]()
图 4.6–高级系统属性
c) 在系统变量下,点击新建按钮新建系统范围变量:
![Figure 4.7 – Environment variables]()
图 4.7–环境变量
d) 指定系统变量的名称为
securekey,其值为BGw3UHkI4z:![Figure 4.8 – securekey system variable]()
图 4.8–securekey 系统变量
e) 通过在命令行中键入以下命令,测试是否已创建新的系统变量:
echo %securekey%前面的命令将在命令提示符中显示
securekey系统变量的值。如果您是 Linux/MacOS 用户,请执行以下步骤:
a) 打开主目录中的
.bashrc文件。键入以下命令以使用 gedit 文本编辑器打开.bashrc文件:gedit ~/.bashrc键入以下命令以使用 vim 文本编辑器打开
.bashrc文件:vi ~/.bashrc.bashrc是一个 shell 脚本,Bash 执行该脚本以初始化交互式 shell 会话。下面的屏幕截图显示了在基于 GNOME 的桌面环境中的默认文本编辑器gedit中打开的
.bashrc文件。这在 Linux 上很常见:![Figure 4.9 – .bashrc file opened for modification]()
图 4.9–.bashrc 文件打开进行修改
b) 使用您选择的代码编辑器(vim 或 gedit),将以下行添加到您的
.bashrc文件中:export securekey=" BGw3UHkI4z"以下屏幕截图显示添加了新环境变量的 gedit 文本编辑器:
![Figure 4.10 – New environment variable]()
图 4.10–新环境变量
c) 保存
.bashrc文件以应用更改。d) 打开一个新终端,然后键入以下命令以验证环境变量现在是否存在:
echo $securekey以下屏幕截图显示新的
securekey环境变量已成功添加:![Figure 4.11 – New Terminal]()
图 4.11–新航站楼
-
Lastly, remove the following line, which declares the
keyvariable:private const string key = "BGw3UHkI4z";提示
对于大规模部署,将机密和密钥存储在环境变量中可能并不理想。作为一种选择,可以使用基于云的密钥存储,例如Azure 密钥库。按照快速入门说明在 Azure Key Vault 中存储您的机密:https://docs.microsoft.com/en-us/azure/key-vault/secrets/quick-create-net 。
现在,让我们了解一下这个配方是如何工作的。
它是如何工作的…
如果攻击者能够获得代码存储库,那么坏的参与者就有可能在代码中看到私钥。我们必须保护这些密钥,并且它们必须从代码存储库外部存储,从而将能够看到它们的人限制在能够访问系统的任何人。在我们的示例网上银行应用中,密钥在代码中明确声明为变量:
private const string key = "BGw3UHkI4z";
这是由于未经授权的访问而导致意外暴露的风险,但可以通过将密钥存储在环境变量中来解决。
在这里,我们创建了一个系统变量,使其在机器范围内可访问,并通过调用Environment.GetEnvironmentVariable方法检索密钥:
FirstName = _cryptoService.Encrypt(Input.FirstName, Environment.GetEnvironmentVariable("securekey", EnvironmentVariableTarget.Machine)),
第一个参数是环境变量的名称,而第二个参数指定存储密钥的位置。可能的enum值为EnvironmentVariableTarget.Machine、EnvironmentVariableTarget.Process和EnvironmentVariableTarget.User。
我们使用EnvironmentVariableTarget.Machine表示我们的变量存储在 Windows 注册表中。它的确切位置是 Windows 注册表HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment。
笔记
您可以使用构建管道将环境变量添加到 web 服务器的过程自动化。如果您使用的是Azure 管道,您可以通过阅读 Microsoft 文档中的定义变量部分了解更多信息 https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch。
还有更多…
ASP.NET Core web 应用开发人员的另一个选择是使用基于云的服务来存储密钥和机密。主要的云提供商提供这种类型的服务,并允许您管理使用硬件安全模块的加密密钥
要将这些服务集成到 ASP.NET web 应用中,您必须订阅并使用其特定的软件开发工具包。以下是其中两个:
- Azure 密钥库:将与此服务集成需要您安装Azure SDK并使用 ASP.NET Core 的Azure 密钥库配置提供程序检索密钥。
- AWS 密钥管理服务(KMS):AWS SDK for.NET将帮助您快速将 KMS 服务添加到 ASP.NET Core web 应用中。
现在,让我们继续下一个食谱。
禁用关键网页的缓存
性能是 ASP.NET Core web 应用的关键指标之一。作为开发人员,我们找到了使页面加载更快的方法,而缓存资源和页面的概念并不是什么新鲜事。浏览器根据从服务器接收的缓存控制指令实现缓存。但是,缓存包含敏感数据的页面存在相关风险。我们必须有选择地确定哪些页面有泄漏信息的危险,并禁用这些页面的缓存。
准备好了吗
我们将使用上一个配方中使用的在线银行应用。使用 Visual Studio 代码,打开位于\Chapter04\cache-data\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤,在管理配置文件页面上禁用缓存。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open the
Areas\Identity\Pages\Account\Manage\Index.cshtml.csfile. Notice that there is no annotation for the page model, which typically controls response caching:namespace OnlineBankingApp.Areas.Identity.Pages.Account.Manage { public partial class IndexModel : PageModel {此页面允许您管理您的个人资料并更改您的个人详细信息,如您的名字、姓氏和出生日期。打开缓存时查看这些页面可能会导致信息泄漏,因为浏览器会缓存数据,除非指示不这样做。
-
To disable caching on this particular Razor page, let's annotate the page mode with the
ResponseCacheattribute:namespace OnlineBankingApp.Areas.Identity.Pages.Account. Manage { [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public partial class IndexModel : PageModel { private readonly UserManager<Customer> _userManager; private readonly SignInManager<Customer> _signInManager;添加
ResponseCache属性将设置缓存控制响应头的指令。
现在,让我们验证一下作为 HTTP 响应的一部分,是否已将缓存控制设置为无缓存、无存储。
正在验证缓存控制 HTTP 响应头
让我们开始吧:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/。* Log in using the following credentials:
a) 。电邮:
stanley.s.jobson@lobortis.cab) 。密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the home page:
![Figure 4.12 – Home page authenticated]()
图 4.12–主页验证
- Click stanley.s.jobson@lobortis.ca from the banner to go to the customer's profile page:
![Figure 4.13 – Profile page]()
图 4.13–配置文件页面
- 按F12打开浏览器的开发者工具。* 进入网络选项卡,在流量列表中选择第一条 HTTP 流量。* 选择单一 HTTP 流量后,滚动右窗格以查看相应的 HTTP 响应安全标头:*
- 打开浏览器并转到
*
图 4.14–缓存控制
缓存控制头现在已添加为 HTTP 响应的一部分,该响应是从我们的示例网上银行 web 应用发送的。
它是如何工作的…
将缓存控制设置为无存储,无缓存告知浏览器不缓存响应。您可以通过使用具有以下属性及其相应值的ResponseCache属性并注释IndexModel来实现这一点:
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
Duration设置为0秒,因此将其匹配的max-age指令设置为相同的值。将max-age赋值为0秒表示应该从 web 服务器重新检索资源。
Location被分配了一个enum值ResponseCacheLocation.None,该值指定浏览器不会缓存资源。
NoStore设置为true时,配置no-store指令 HTTP 响应头,告知客户端不缓存资源。
提示:
您可以通过创建CacheProfiles集中配置 ASP.NET Core web 应用的缓存行为:
using Microsoft.AspNetCore.Mvc;
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(options =>
{
options.CacheProfiles.Add("NoCache",
new CacheProfile()
{
Duration = 30,
Location = ResponseCacheLocation. None,
NoStore = true
});
});
前面配方中相同的值已传递到CacheProfile实例以禁用缓存。*****
五、XML 外部实体
可扩展标记语言(XML)是用于定义数据的标准标记语言。XML 也是 ASP.NET Core web 应用可以用来解析信息的格式。为了实现这一点,开发人员可以使用框架中随时可用的任意数量的.NET XML 解析器。
作为输入源的 XML 很可能容易被恶意数据注入。名为XML 外部实体(XXE的功能允许 XML 使用 URL 或文件路径定义自定义实体。这种用 XML 表示外部实体的能力可能被滥用或利用。不受限制的外部实体引用可使攻击者将敏感信息和文件发送到应用的受信任域之外,并发送到行为人控制的服务器。此漏洞的存在可能导致拒绝服务(DoS攻击,使整个应用无法访问,因为请求泛滥,或文件包含攻击,对手可以未经授权访问文件。
为了确保代码的安全性并防止这些类型的基于 XML 的注入攻击,必须使用XML 模式(XSD验证 XML。XSD 的使用确保了 XML 与所需格式的一致性。此外,必须仔细配置和设置要使用的.NET 解析器,以避免 XML 解析器出现任何意外行为。
在本章中,我们将介绍以下配方:
- 启用 XML 验证
- 用
XmlDocument固定 XXE 注入液 - 用
XmlTextReader固定 XXE 注入液 - 用LINQ到XML固定 XXE 注入
这些方法将教会我们如何向 ASP.NET Core web 应用添加 XML 模式验证,以及如何修复代码中允许注入和处理外部 XML 实体的各种漏洞。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter05 。
启用 XML 验证
一个XSD指定了 XML 的组成方式。模式有助于定义 XML 结构,并防止不需要的元素、属性和文本。如果没有 XSD,.NET 解析器将盲目处理 XML 数据,并增加代码中 XXE 注入漏洞的风险。
本食谱将教您如何创建使用 XSD 和验证 XML 数据的方法。
准备好了吗
为了完成本章中的菜谱,我们需要一个示例网上银行应用。
打开命令 shell 并通过克隆 ASP.NET Secure Codeing Cookbook 存储库下载示例网上银行应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter05\missing-validation\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
怎么做…
让我们来看看这个食谱的步骤:
-
在“开始练习”文件夹中,通过键入以下命令来启动 Visual Studio 代码:
code . -
Under the
wwwrootdirectory, right-click and select New File. Name the fileKnowledgebase.xsd:![Figure 5.1 – Adding a new file]()
图 5.1–添加新文件
-
在
Knowledgebase.xsd文件中添加以下标记(该xsd文件的全部内容可在\Chapter05\missing-validation\after\OnlineBankingApp下的已完成练习文件夹中找到):<?xml version="1.0" encoding="utf-8"?> <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="knowledgebase"> <xs:complexType> <xs:sequence> <xs:element maxOccurs="unbounded" name="knowledge"> <xs:complexType> <xs:sequence> <xs:element name="topic"> <xs:complexType> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="lang" type="xs:string" use="required" /> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> // markup removed for brevity -
既然我们已经创建了模式,我们将使用这个
xsd文件来验证Knowledgebase.xml的格式,它包含我们知识库的数据。 -
打开
\Services\KnowledgebaseServices.cs并添加对以下命名空间的引用:using System.Xml.Schema; using System.IO; -
这些名称空间允许您使用验证 XML 模式所需的类,并为 XML 文件上的文件输入输出操作提供一些方法。
-
我们需要重构
Search方法中的代码,以便它可以使用我们在步骤 3中创建的模式。在声明webroot变量的位置之后和声明file的位置之前插入以下代码:var webRoot = _env.WebRootPath; var schemaSet = new XmlSchemaSet(); var xsdFile = System.IO.Path.Combine(webRoot, "Knowledgebase.xsd"); using (System.IO.FileStream stream = File.OpenRead(xsdFile)) { schemaSet.Add(XmlSchema.Read(stream, (s, e) => { var x = e.Message; })); } XmlReaderSettings settings = new XmlReaderSettings(); settings.ValidationType = ValidationType.Schema; settings.Schemas = schemaSet; settings.DtdProcessing = DtdProcessing.Ignore; var file = System.IO.Path.Combine(webRoot, "Knowledgebase.xml"); -
声明一个
XmlReader对象,该对象将以file和settings作为前面代码行中的参数:XmlReader reader = XmlReader.Create(file, settings); -
Modify this line of code so that the
XmlDocumentinstance will load the reader instead of the file:xmlDoc.Load(reader);XmlDocument对象现在加载XmlReader,其中包括模式验证。
它是如何工作的…
knowledgebase.xsd文件包含knowledgebase.xml中允许的有效元素和属性。这个 XSD 还描述了元素的预期数据类型。例如,<xs:element name="sensitivity" type="xs:string" />期望sensitivity元素为string数据类型。knowledgebase.xml文件应遵循此格式。否则,验证过程将失败。
我们在\Services\KnowledgebaseServices.cs中添加了对System.Xml.Schema名称空间的引用,该名称空间包含我们需要验证的类。从这个引用中,我们使用了XmlSchemaSet类来存储knowledgebase.xsd模式。XmlSchemaSet类可以包含多个模式,但我们的配方将只使用一个模式。
然后,我们用File.OpenRead(xsdFile)方法创建了一个FileStream对象。xsdFile对象表示knowledgebase.xsd并作为参数传递给File.OpenRead。然后将FileStream对象提供给XmlSchema.Read方法,并将knowledgebase.xsd添加到schemaSet集合中。
XmlReaderSettings属性的ValidationType被赋值为ValidationType.Schema,该值设置新的XmlReader以使用 XSD 模式执行验证。
通过XmlReader.Create方法,我们使用前面几行中定义的XmlReaderSettings属性创建了XmlReader的reader实例。最后,我们将 reader 对象传递给XmlDocument实例,让应用开始解析knowledgebase.xml文件。
笔记
与 XML 类似,XSD 也可以具有外部引用,并且可以来自不受信任的资源。需要进行额外的验证,以确保源是可信的,并且您没有使用来自恶意 XSD 的恶意字符串。
还有更多…
XML 的另一种形式是可扩展样式表语言转换,或者简称为的XSLT。该语言用于将 XML 文档转换为其他文档格式,如纯文本文件或 HTML。
您可以使用System.Xml.Xsl命名空间中的XslCompiledTransform类将 XSLT 加载到 ASP.NET web 应用中。调用Load和Transform方法加载 XSLT 并将其转换为特定格式:
XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load("Knowledgebase.xsl");
xslt.Transform("Knowledgebase.xml", "Knowledgebase.html");
然而,与任何形式的输入一样,XSLT 也可能被污染或破坏以加载恶意的 XXE。要安全地编译 XSLT,请传递一个null``XmlResolver来停止解析外部资源,并将 XmlReaderSetting 的DtdProcessing设置为Ignore。
为了防止 XSLT 嵌入脚本块和使用危险的document()函数,我们将 XSLT 设置的EnableScript和EnableDocumentFunction设置为false:
XslCompiledTransform xslt = new XslCompiledTransform();
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Ignore;
var file = System.IO.Path.Combine(webRoot, "Knowledgebase.xsl");
XmlReader reader = XmlReader.Create(file, settings);
XsltSettings xsltSettings = new XsltSettings();
xsltSettings.EnableDocumentFunction = false;
xsltSettings.EnableScript = false;
xslt.Load(reader, xsltSettings, null);
笔记
默认情况下,EnableDocumentFunction和EnableScript属性被禁用并设置为false。
在将这些对象传递到重载的Load方法之前,使用正确的设置配置这些对象有助于防止 XSLT 注入和 XXE 攻击。
用 XmlDocument 固定 XXE 注入剂
XmlDocument类实际上是.NET 应用的 XML 解析器。这个 XML 解析器对象通常用于加载、修改和删除内存中的 XML。它有一个XmlResolver属性,允许使用外部 XML 资源,如 DTD。
文档类型定义,最常见的称为DTD,类似于 XML 文件,但包含 XML 的组成或结构信息。它可以有一个ENTITY元素,可以是内部的,也可以是外部的。当XDocument使用 DTD 解析 XML 文件时,该 XML 解析器将处理该文件及其ENTITY声明。
让我们看一下带有恶意注入的ENTITY声明的 XML 文件的某些内容。这是一个已知的十亿笑声攻击的经典例子,这是一种拒绝服务(DoS)攻击,目标是XmlDocument等 XML 解析器。加载此 XML 将导致 ASP.NET Core web 应用崩溃或无响应:
<?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;">
]>
<lolz>&lol3;</lolz>
此外,意外地设置您的不安全自定义XmlResolver可能允许将来自不受信任源或主机的 DTD 包含在knowledgebase.xml解析中,可能导致XXE 注入。
这个方法将向您展示如何禁用 DTD,从而使您的代码在解析 XML 时更加安全。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter05\xxe-injection01\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
打开
Services\KnowledgebaseService.cs文件,注意分配给XmlUrlResolver对象的行:XmlUrlResolver resolver = new XmlUrlResolver(); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.XmlResolver = resolver; xmlDoc.Load(file); -
Assign the
XmlResolverproperty ofxmlDoctonull:XmlDocument xmlDoc = new XmlDocument(); xmlDoc.XmlResolver = null; xmlDoc.Load(file);将
XmlResolver设置为null将禁止 DTD 加载到knowledgebase.xml文件中。笔记
当 XML 文档中存在外部实体或DTD时,当应用尝试解析XML时会抛出
XMLException。
它是如何工作的…
在我们的OnlineBankApp示例应用中,XMLResolver被实例化并分配给 xmlDoc 的XmlResolver属性。为XmlResolver属性设置值表示允许加载 DTD 或解析外部实体引用。
然而,为了缓解代码中的这个漏洞,我们必须取消XmlResolver引用,从而防止恶意 DTD 被加载。
还有更多…
如果需要解析 ASP.NET Core web 应用中的 DTD,则必须使用XmlSecureResolver类并将其实例分配给XmlDocument的XmlResolver属性。XmlSecureResolver是XmlResolver类的安全实现,限制对资源的访问。
要使用此类实现安全的 DTD 解析,只需将 URL 作为XmlSecureResolver构造函数的第二个参数传递,即可定义允许的资源。明确声明了https://localhost:5001URL,表示我们只允许来自此 URL 的资源:
XmlSecureResolver resolver = new XmlSecureResolver(new XmlUrlResolver(), "https://localhost:5001");
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.XmlResolver = resolver;
xmlDoc.Load(file);
XmlSecureResolver对象用PermissionSet包装XmlResolver对象,指定允许的访问。在内部,它调用PermitOnly方法,正如其名称所示,该方法设置了不会导致调用代码失败的权限。
用 XmlTextReader 固定 XXE 注入液
与XmlDocument类似,另一个快速、非缓存、仅转发到 XML 的解析器选项是XmlTextReader。这种高性能解析器的一个主要缺点是缺乏数据验证。XmlTextReader还允许您在默认情况下处理 DTD,如果您的 XML 源不可信,这可能会引起关注。
此配方将向您展示如何使用XmlTextReader.禁用 DTD 处理
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter05\xxe-injection02\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open the
Services\KnowledgebaseService.csfile. This version of theOnlineBankingAppsample solution is usingXmlTextReaderto parse theKnowledgebase.xmlfile:XmlTextReader xmlReader = new XmlTextReader(file); xmlReader.DtdProcessing = DtdProcessing.Parse; XPathDocument xmlDoc = new XPathDocument(xmlReader);将
xmlReader的DtdProcessing属性赋值给DtdProcessing.Ignore的enum值:XmlTextReader xmlReader = new XmlTextReader(file); xmlReader.DtdProcessing = DtdProcessing.Ignore; XPathDocument xmlDoc = new XPathDocument(xmlReader);将
DtdProcessing属性设置为DtdProcessing.Ignore将阻止处理 DTD 并忽略标记。DOCTYPE是一种标记形式,用于通知浏览器文档使用的 HTML 版本。
它是如何工作的…
遵循以下步骤:
- 当 XmlTextReader 的
DtdProcessing属性设置为enum值DtdProcessing.Parse时,这表示将处理 DTD。如果一个坏角色将恶意实体引用节点注入到knowledgebase.xml文件中,那么允许处理 DTD 可能是危险的。 - 指定
DtdProcessing.Ignore的DtdProcessing属性使处理 DTD 变得不可能,从而使代码更加安全。
用 LINQ 到 XML 修复 XXE 注入
语言集成查询或LINQ是.NET 框架内的一个 API,为编写声明性代码提供类似查询的语法。LINQ 有不同的风格,LINQ 到 XML 就是其中之一。LINQ to XML 是一个内存中的 XML 解析器,允许您执行 XML 转换——从修改元素和节点到序列化。
一般来说,LINQ 到 XML 不受 XXE 注入的影响。XDocument类默认已禁用 DTD 处理。但是,当使用不安全的 XML 解析器(如XmlReader)实例化时,这可能是不安全的。此配方将向您展示如何在 LINQ to XML 代码中发现安全漏洞,并通过禁用 DTD 处理来修复该漏洞。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter05\xxe-injection03\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以在此文件夹中执行使用 LINQ to XML 修复 XXE 注入的步骤。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open the
Services\KnowledgebaseService.csfile. This version of theOnlineBankingAppsample solution is usingLinq to XMLto parse theKnowledgebase.xmlfile.注意
XDocument类的使用和解析 XML 的类似查询的方法:XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Parse; settings.MaxCharactersFromEntities = 1024; settings.MaxCharactersInDocument = 2048; XmlReader reader = XmlReader.Create(file, settings); XDocument xmlDoc = XDocument.Load(reader); var query = from i in xmlDoc.Element("knowledgebase") .Elements("knowledge") where (i.Element("topic").ToString() .Contains(input) == true || i.Element("description").ToString() .Contains(input) == true) && i.Element("sensitivity").ToString() .Contains("Public") == true select new { Topic = (string)i.Element("topic"), Description = (string)i.Element("description") }; -
XmlReader类的Create方法被一个易受攻击的XmlReaderSettings调用。正如我们在前面的配方中看到的,我们必须禁用 DTD 处理。这可以通过将DtdProcessing设置为DtdProcessing.Prohibit:XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Prohibit; settings.MaxCharactersFromEntities = 1024; settings.MaxCharactersInDocument = 2048;的枚举值来实现
-
将
XmlReaderSettings对象的DtdProcessing属性从DtdProcessing.Parse更改为DtdProcessing.Prohibit将阻止 DTD 处理,并且在 XML 中存在 DTD 时也会抛出XmlException。 -
现在,我们必须将
XmlReaderSettings实例的MaxCharactersFromEntities赋值为1024:XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Prohibit; settings.MaxCharactersFromEntities = 1024; settings.MaxCharactersInDocument = 2048; -
分配
MaxCharactersFromEntities将限制扩展实体的大小并防止滥用。 -
We must also explicitly assign
MaxCharactersInDocumentof theXmlReaderSettingsobject with a value of2048:XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Prohibit; settings.MaxCharactersFromEntities = 1024; settings.MaxCharactersInDocument = 2048;将
MaxCharactersInDocument属性的值指定为2048表示 XML 文件中允许的最大字符数为2048。同样,这有助于防止攻击者可能滥用。笔记
添加一个
try-catch语句来处理XmlException的可能性。始终在代码中练习安全的错误处理。本书的最后一章将详细介绍这一最佳实践。
它是如何工作的…
在KnowledgebaseService类中,我们有一个Search方法对整个Knowledgebase.xml文件执行搜索,其中包含帮助查询数据。当用户点击网页上的搜索时,它会调用此方法并创建XmlReader和XDocument类的实例。
XDocument用XmlReader实例化。XmlReader的属性基于知识库 XML 数据及其XmlReaderSettings。虽然这是用 XML 数据填充解析器的有效方法,XmlReader的XmlReaderSettings将DtdProcessing属性设置为DtdProcessing.Parse,从而使用不安全的解析器设置XDocument对象。此设置导致代码易受 XXE 注入攻击。
为了解决这个问题,我们必须选择一个更好的财产价值——要么是DtdProcessing.Ignore要么是DtdProcessing.Prohibit——并将其分配给XmlReaderSettings的DtdProcessing。这两个值都可以防止危险的 DTD 处理。我们在前面的配方中涵盖了DtdProcessing.Ignore属性值,因此我们选择了DtdProcessing.Prohibit。
要管理通过 XML 解析器发生的 DTD,请在DtdProcessing.Ignore上选择DtdProcessing.Prohibit属性值。拥有DtdProcessing.Prohibit会引发XmlExceptions,您可以使用 try-catch 块来处理,而DtdProcessing.Ignore则完全忽略 DTD。
假设攻击者能够输入包含大量数据的任意 XML 文件。这可能会导致系统消耗大量计算资源,从而导致 DOS 攻击。我们可以通过为XmlReaderSettings的MaxCharactersFromEntities和MaxCharactersInDocument属性分配一个合理的值来防止这种情况发生。这些属性限制了 XML 及其元素和属性的扩展大小。
六、访问控制中断
授权与认证一样重要和必要。它定义了经过身份验证的用户可以执行的操作,资源和网页需要具有定义的权限来限制未经授权的访问。权限绕过和缺少或不正确的访问控制是在 ASP.NET Core web 应用中发现的一些损坏的访问控制漏洞。
在本章中,我们将介绍以下配方:
- 固定不安全的直接对象引用(IDOR)
- 修复不正确的授权
- 修复丢失的访问控制
- 修复开放重定向漏洞
本章结束时,您将学习如何使用 ASP.NET Core 中的内置授权机制。您将正确实现基于角色的授权,以防止未经授权访问 web 应用中的资源。此外,您还将看到如何利用更安全的重定向方法来防止开放式重定向攻击。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库(DB引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter06 。
固定 IDOR
当访问数据库中的记录时,我们通常使用标识符(ID)的形式来唯一标识数据集。数据库的设计和结构依赖于这些键,有时它们很容易被猜测或枚举。对手可以在您对 ASP.NET Core 网页的请求中找到这些标识符。如果没有足够的访问控制保护,恶意用户可以查看、修改或删除这些记录,最坏的情况是删除这些记录。
在此配方中,我们将发现代码中的 IDOR 漏洞,并通过使用经过身份验证的客户的身份来缓解问题。
准备好了吗
对于本章的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆ASP.NET-Core-Secure-Coding-Cookbook存储库下载示例网银应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter06\insecure-direct-object-references\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
让我们看看 IDOR 漏洞是如何被利用的。
测试 IDOR
以下为步骤:
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Fundtransfers/Details?id=1。* Log in using the following credentials:
a) 电邮:
stanley.s.jobson@lobortis.cab) 密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to Stanley's fund transfer details page, as shown in the following screenshot:
![Figure 6.1 – Fund transfer details page]()
图 6.1–资金转账详情页面
- Click on Logout to log out from the sample solution, as shown in the following screenshot:
![Figure 6.2 – Logout link]()
图 6.2–注销链接
- 转到
https://localhost:5001/FundTransfers/Details?id=1。* Now, log in using Axl's credentials:
a) 电邮:
axl.l.torvalds@ut.netb) 密码:
6GKqqtQQTii92ke!- 注意下面的截图,Axl 可以看到 Stanley 的资金转账详情页面:*
- 打开浏览器并转到
*
图 6.3–未经授权的访问
前面的测试表明,此页面容易受到 IDOR 安全漏洞的影响。
在此配方中,我们将通过添加验证检查来确定特定用户是否可以查看资金转账详细信息页面,从而修复代码中的 IDOR 漏洞。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open
Models\FundTransfer.csand change theIDproperty fromintto aGuidtype. Globally unique identifiers (GUIDs) are unique identifiers and are harder to guess:[Key] public Guid ID { get; set; }用
Key属性注释ID属性,使该属性成为实体框架识别的主键。 -
在
Services文件夹下,创建一个新文件并将其命名为FundTransferIsOwnerAuthorizationHandler.cs。 -
在
FundTransferIsOwnerAuthorizationHandler.cs中,添加对以下名称空间的引用:using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using System.Threading.Tasks; using OnlineBankingApp.Models; -
Next, define a
FundTransferIsOwnerAuthorizationHandlerclass that inherits fromAuthorizationHandler:namespace OnlineBankingApp.Authorization { public class FundTransferIsOwnerAuthorizationHandler : AuthorizationHandler< FundTransferOwnerRequirement, FundTransfer>{ } }顾名思义,授权处理程序处理授权,在前面突出显示的代码中,它确定用户是否具有访问权限。
-
使用依赖注入(DI),使用
UserManager服务能够从当前登录的客户UserManager<Customer> _userManager; public FundTransferIsOwnerAuthorizationHandler (UserManager<Customer> userManager){ _userManager = userManager; }中检索用户 ID 信息
-
在
FundTransferIsOwnerAuthorizationHandler类中,定义一个Task对象,该对象将使用传递的requirement和resource参数处理授权检查:protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FundTransferOwnerRequirement requirement, FundTransfer resource){ if (context.User == null || resource == null){ return Task.CompletedTask; } if (resource.CustomerID == _userManager.GetUserId (context.User)){ context.Succeed(requirement); } return Task.CompletedTask; } } -
Define a
FundTransferOwnerRequirementclass that will inherit from theIauthorizationRequirementempty marker interface within the sameFundTransferIsOwnerAuthorizationHandler.csfile:public class FundTransferOwnerRequirement : IAuthorizationRequirement { } }FundTransferOwnerRequirement不需要有任何属性或数据,因此我们将保留该类为空。 -
打开
Startup.cs并包含以下名称空间引用:using OnlineBankingApp.Authorization; using Microsoft.AspNetCore.Authorization .Infrastructure; -
在
ConfigureServices中,添加一个新的授权策略,并注册我们在步骤 3中创建的授权处理程序:
```cs
services.AddAuthorization(options => {
options.AddPolicy("Owner", policy =>
policy.Requirements.Add(new FundTransferOwnerRequirement()));
});
services.AddScoped<IAuthorizationHandler, FundTransferIsOwnerAuthorizationHandler>();
```
- 接下来,打开文件
\Pages\FundTransfers\Details.cshtml.cs并添加以下名称空间引用:
```cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
```
- 通过 DI,将我们在步骤 5中注册的授权服务中突出显示的以下代码添加到
DetailsModel构造函数中:
```cs
protected IAuthorizationService _authorizationService { get; }
protected UserManager<Customer> _userManager { get; }
public DetailsModel(OnlineBankingApp.Data .OnlineBankingAppContext context,
IAuthorizationService authorizationService,
UserManager<Customer> userManager)
{
_context = context;
_userManager = userManager;
_authorizationService = authorizationService;
}
```
- Refactor the whole code under the
OnGetAsyncpage handler:
```cs
public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (!id.HasValue){
return NotFound();
}
if (!User.Identity.IsAuthenticated){
return Challenge();
}
fundTransfer = await _context.FundTransfer
.Where(f => f.ID == id)
.Include(f => f.Customer)
.OrderBy(f => f.TransactionDate)
.FirstOrDefaultAsync<FundTransfer>();
var isAuthorized = await _authorizationService.AuthorizeAsync (User, fundTransfer,"Owner");
if (!isAuthorized.Succeeded){
return Forbid();
}
return Page();
}
```
通过我们执行的步骤,我们使用基于策略的授权方法实现了一种更健壮的授权方式。
重复*测试 IDOR*部分中的步骤以验证修复是否有效,但不要使用 IDOR 易受攻击的**统一资源定位器**(**URL**,而是转到[https://localhost:5001/FundTransfers/Details?id=7c281d46-f2ab-4027-a4d4-3B97A60012C](https://localhost:5001/FundTransfers/Details?id=7c281d46-f2ab-4027-a4d4-3bb97a60012c)和您应在屏幕上看到以下消息:

图 6.4–拒绝访问消息
请注意,Axl 的帐户不再可以访问 Stanley 的资金转账详细信息页面,并被重定向到拒绝访问页面。
它是如何工作的…
首先,我们将FundTransfer主键更改为不易猜测的类型。我们使用Guid类型允许我们拥有唯一 ID(UID)作为每次资金转账的Key:
[Key]
public Guid ID { get; set; }
然后,我们通过首先创建一个授权处理程序来实现基于策略的授权。在FundTransferIsOwnerAuthorizationHandler类中,是确定资源(资金转账)CustomerID是否与客户用户 ID 匹配的代码。如果满足要求,调用AuthorizationHandlerContext的Succeed方法表示评估成功:
if (resource.CustomerID == _userManager.GetUserId(context.User)){
context.Succeed(requirement);
}
授权处理程序注册为服务,分别使用AddScoped和AddPolicy方法添加预配置策略:
services.AddAuthorization(options => {
options.AddPolicy("Owner", policy =>
policy.Requirements.Add(new FundTransferOwnerRequirement()));
});
services.AddScoped<IAuthorizationHandler, FundTransferIsOwnerAuthorizationHandler>();
我们在DetailsModel页面模型中通过 DI 使用这些服务。
修复不当授权
错误地使用 ASP.NET Core 的授权组件可能会导致代码不安全。授权特性提供了一种简单的声明性方式来强制授权,但是在实现时可能会出错。在此配方中,我们将在示例网上银行应用中正确实现 ASP.NET Core 的基于角色的授权功能。
运行示例应用以验证没有生成或编译错误。在命令行中,导航到位于\Chapter06\improper-authorization\before\OnlineBankingApp的示例应用文件夹。
让我们看看不恰当的授权如何导致某人使用客户无权使用的功能。
测试授权不当
以下是步骤:
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/FundTransfers/Create。* Log in using the following credentials:
a) 电邮:
axl.l.torvalds@ut.netb) 密码:
6GKqqtQQTii92ke!- 一旦通过身份验证,您将被重定向到可以进行资金转账的页面。*
- 打开浏览器并转到
*我们的样本网上银行解决方案只创建了 Axl 的客户帐户;因此,他的角色是Customer和PendingCustomer。在 Axl 的账户进入ActiveCustomer角色之前,他不应该进行资金转账。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码,在\Chapter06\missing-access-control\before\OnlineBankingApp\处打开样本网银应用文件夹。
您也可以在此文件夹中执行修复不当授权配方的步骤。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open the
\Pages\FundTransfers\Create.cshtml.csfile and notice theAuthorizeannotation on top of theCreateModelclass:namespace OnlineBankingApp.Pages.FundTransfers { [Authorize(Roles = "Customer,ActiveCustomer")] public class CreateModel : AccountPageModel { private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context; public CreateModel (OnlineBankingApp.Data .OnlineBankingAppContext context) { _context = context; } // code removed for brevityAuthorize注释似乎被正确使用,但并不完全正确。CreateModel页面模型只对具有Customer或角色的客户开放。在此格式中设置Authorize注释意味着任何角色的客户都可以汇款,根据我们的业务规则,这不是我们所期望的,只允许活跃客户进行资金转账。 -
使用以下代码更改
Authorize注释的格式:namespace OnlineBankingApp.Pages.FundTransfers { [Authorize(Roles = "Customer")] [Authorize(Roles = "ActiveCustomer")] public class CreateModel : AccountPageModel { private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context; Public CreateModel(OnlineBankingApp.Data .OnlineBankingAppContext context) { _context = context; } // code removed for brevity -
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Fundtransfers/Create。* Log in with the following credentials:
a) 。电邮:
axl.l.torvalds@ut.netb) 。密码:
6GKqqtQQTii92ke!- 请注意,您将被重定向到拒绝访问页面,如下图所示:*
- 打开浏览器并转到
*
图 6.5–访问被拒绝页面
设置AuthorizeAttribute属性可在CreateModel页面模型中配置必要的授权。这要求经过身份验证的用户同时具有Customer和ActiveCustomer角色。
它是如何工作的…
声明性角色检查使 web 开发人员能够轻松地在页面模型中添加授权,但是注释之间有很大的区别。例如,看看这个:
[Authorize(Roles = "Customer,ActiveCustomer")]
现在,将其与以下注释进行对比:
[Authorize(Roles = "Customer")]
[Authorize(Roles = "ActiveCustomer")]
第一个表示具有Customer或ActiveCustomer角色的经过身份验证的用户可以访问资金转账页面。后者规定客户需要两个角色才有权汇款。
提示
基于策略的授权检查也是伴随声明性授权的必要技术,以确保用户有权查看资金转账。有关如何实现此类授权的更多信息和详细信息,请参阅修复 IDOR配方。
修复丢失的访问控制
访问控制漏洞允许恶意参与者只需注册帐户并获得身份验证即可访问您的 ASP.NET Core web 应用。此安全漏洞可能导致未经授权访问敏感信息。
在此配方中,我们向示例网上银行应用添加角色,以集成基于策略的授权。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码,在\Chapter06\missing-access-control\before\OnlineBankingApp\处打开样本网银应用文件夹。
您还可以在此文件夹中执行修复缺少的访问控制配方的步骤。
怎么做…
让我们来看看这个食谱的步骤。
-
在启动练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open the
\Pages\FundTransfers\Create.cshtml.csfile and notice theAuthorizeannotation on top of theCreateModelclass:namespace OnlineBankingApp.Pages.FundTransfers { [Authorize] public class CreateModel : AccountPageModel { private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context; public CreateModel(OnlineBankingApp.Data .OnlineBankingAppContext context) { _context = context; } // code removed for brevityCreateModel类中的Authorize属性提供了最基本的授权,表明此 Razor pages 模型需要授权。但是,由于缺乏关于哪些类型的客户可以进行资金转账的定义角色,因此对手有可能滥用此授权。 -
We need to implement policy-based authorization with criteria defined based on the current roles that our customer has. Under the
Modelsfolder, create a new file, name itPrincipalPermission.cs, and add the following code:using System; using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; using OnlineBankingApp.Models; namespace OnlineBankingApp.Authorization{ public static class PrincipalPermission{ public static List <Func<AuthorizationHandlerContext, bool>> Criteria = new List<Func <AuthorizationHandlerContext, bool>> { CanCreateFundTransfer }; public static bool CanCreateFundTransfer (this AuthorizationHandlerContext ctx){ return ctx.User.IsInRole (Role.ActiveCustomer.ToString()); } } }在前面的代码片段中,我们使用
Func来实现策略。Func是一名代表,将指向我们的CanCreateFundTransfer方法。我们还创建了一个List<Func<AuthorizationHandlerContext, bool>>实例来为我们的策略配置Criteria列表。我们将CanCreateFundTransfer方法定义为我们的标准之一,表明只有ActiveCustomer角色的客户才能创建资金转账。笔记
您可以为客户定义更多的条件,使其能够提交资金转账,但为了简化示例,我们将使用客户的当前角色。
-
打开
Startup.cs,在ConfigureServices中添加对OnlineBankingApp.Authorization的引用,这是我们PrincipalPermission类的名称空间:using OnlineBankingApp.Authorization; -
Include the following highlighted code in the authorization middleware:
services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); foreach (var criterion in PrincipalPermission .Criteria) { options.AddPolicy(criterion.Method.Name, policy => policy.RequireAssertion(criterion)); } });我们将循环到我们定义的每个标准列表中,并为每个标准创建一个授权策略。
-
Open
Pages\FundTransfers\Create.cshtml.csand annotate theCreateModelpage model with the highlighted code:namespace OnlineBankingApp.Pages.FundTransfers { [Authorize(Policy = nameof(PrincipalPermission .CanCreateFundTransfer))] public class CreateModel : AccountPageModel { // code removed for brevity放置前面突出显示的属性将应用我们添加到授权服务的授权策略。
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Fundtransfers/Create。* Log in with the following credentials:
a) 电邮:
axl.l.torvalds@ut.netb) 密码:
6GKqqtQQTii92ke!请注意,用户被重定向到
https://localhost:5001/Identity/Account/AccessDenied?ReturnUrl=%2FFundTransfers%2FCreate拒绝访问URL:* - 打开浏览器并转到
*
图 6.6–具有 PendingCustomer 角色的用户的访问被拒绝页面
Axl 预先分配了一个PendingCustomer角色(请参见Models\SeedData.cs,这将阻止他根据我们创建的策略提交资金转账。
它是如何工作的…
基于策略的方法为 ASP.NET Core web 开发人员提供了定义授权矩阵所需的粒度。在前面的配方中,我们使用了一个简单的示例,使用角色作为授权策略的标准。在执行一项政策时,我们提供了一个包含我们定义的每个Criteria的Func<AuthorizationHandlerContext, bool>的List:
public static List<Func<AuthorizationHandlerContext, bool>> Criteria = new List<Func <AuthorizationHandlerContext, bool>>
{
CanCreateFundTransfer,
CanViewFundTransfer
};
Criteria表示将用于设置条件接收的委托。在我们的案例中,我们将使用客户的角色作为标准,但如有必要,您可以扩展它:
public static bool CanCreateFundTransfer(this AuthorizationHandlerContext ctx)
{
return ctx.User.IsInRole(Role.ActiveCustomer .ToString());
}
最后,我们使用RequireAssertion策略以Criteria的List构建我们的策略:
foreach (var criterion in PrincipalPermission.Criteria)
{
options.AddPolicy(criterion.Method.Name,
policy => policy.RequireAssertion(criterion));
}
修复开放重定向漏洞
用户可以被诱骗点击 ASP.NET Core web 应用生成的链接,但这最终会将他们重定向到恶意网站。当用户控制的参数确定要重定向到的 URL 没有验证或白名单时,可能会发生打开重定向。在此配方中,我们将通过使用更安全的重定向方法来补救代码中的开放重定向攻击风险。
首先,让我们来看看如何使用开放重定向漏洞。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码,在\Chapter06\unvalidated-redirect\before\OnlineBankingApp\处打开样本网银应用文件夹。
您还可以针对修复打开重定向漏洞配方执行此文件夹中的步骤。
测试开放重定向
以下是步骤:
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Identity/Account/Login?ReturnUrl=https://www.packtpub.com。* Log in using the following credentials:
a) 电邮:
stanley.s.jobson@lobortis.cab) 密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the Packt Publishing website.
前面的测试表明,此页面容易受到开放重定向攻击。*
- 打开浏览器并转到
*## 怎么做…
让我们来看看这个食谱的步骤。
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open
Areas\Identity\Pages\Account\Login.cshtml.csand notice theRedirectmethod call:public async Task<IActionResult> OnPostAsync(string url = null) { . . . . // code removed for brevity var signInResult = await _signInManager .PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); if (signInResult .Succeeded) { _log.LogInformation("User logged in."); if (string.IsNullOrEmpty(HttpContext .Session.GetString(SessionKey))) { HttpContext.Session.SetString(SessionKey, Input.Email); } return Redirect(url); } // code removed for brevity调用时,
Redirect方法向浏览器发送临时重定向响应。在没有 URL 验证的情况下,只要被欺骗的客户单击恶意 URL,URL 重定向就会被滥用并发送到攻击者控制的网站。 -
Another security flaw found in this sample Online Banking app is in its logout page redirection. Open
Areas\Identity\Pages\Account\Logout.cshtml.csand go to theOnGetmethod of the page method:public async Task<IActionResult> OnGet(string url = null) { await _signInManager.SignOutAsync(); _log.LogInformation("User logged out."); if (url != null) { return Redirect(url); } else { return RedirectToPage(); } } // code removed for brevity同样地,重定向是无效的,对手可以创建一个 URL,该 URL 可以重定向到通过网络钓鱼或其他欺骗手段提供的黑客控制的网站。
-
To remediate these security flaws, open
Areas\Identity\Pages\Account\Login.cshtml.csand change theRedirectmethod toLocalRedirect:if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var signInResult = await _signInManager .PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); if (signInResult.Succeeded) { _log.LogInformation("User logged in."); if (string.IsNullOrEmpty (HttpContext.Session.GetString(SessionKey))) { HttpContext.Session.SetString(SessionKey, Input.Email); } return LocalRedirect(url); } // code removed for brevityLocalRedirect方法执行相同的重定向,只是当 URL 试图重定向到非本地网站时,它会抛出InvalidOperationException异常。 -
Another way to fix this security bug is to use the
Url.IsLocalUrlmethod. OpenAreas\Identity\Pages\Account\Logout.cshtml.csand go to theOnGetmethod of the page method. Change the method fromOnGettoOnPostfor this method to be invoked on HyperText Transfer Protocol (HTTP)POSTrequests, not HTTPGETrequests. Replace theRedirectmethod call with a validation check using theIsLocalUrlmethod:public async Task<IActionResult> OnPost(string url = null) { await _signInManager.SignOutAsync(); _log.LogInformation("User logged out."); if (url != null) { if (Url.IsLocalUrl(url)) return Redirect(url); else return RedirectToPage(); } else { return RedirectToPage(); } } // code removed for brevity对
Url.IsLocalUrl方法的调用会检查 URL 是否为本地 URL,从而防止客户冒被重定向到任意 URL 的风险。
它是如何工作的…
恶意网站可能模仿合法网站,欺骗用户使用其凭据登录,最终窃取受害者的用户名和密码。我们同时使用LocalRedirect和Url.IsLocalUrl方法,取代了危险的Redirect方法来验证作为参数接收的 URL。实现这些更安全的功能可以保护我们不被重定向到不需要的 URL。
如果某些用例需要将用户重定向到外部 URL,则必须应用白名单验证技术来确定是否允许 URL。*****
七、安全配置错误
如果在任何应用层(尤其是代码层)中禁用安全控制时出现疏忽,可能会使 ASP.NET Core web 应用容易受到各种各样的攻击。忽略在生产中禁用调试、无意中记录跟踪、cookie 中缺少必要的属性以及 HTTP 安全头只是安全配置错误的几个根本原因。强化 web 应用的安全性从代码开始,如果操作不正确,也可能是应用的薄弱环节。
在本章中,我们将介绍以下配方:
- 在非开发环境中禁用调试功能
- 修复禁用的安全功能
- 禁用不必要的功能
- 通过错误消息修复信息暴露
- 通过不安全的 cookie 修复信息泄露
在本章结束时,您将了解如何通过关闭代码中的调试、添加安全功能以及通过正确的应用设置阻止不必要的信息泄漏,从而防止安全配置错误。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter07 。
在非开发环境中禁用调试功能
在编写代码和运行测试时,调试是 web 开发人员任务中必不可少的部分。ASP.NET Core 使开发人员能够轻松访问配置,该配置将使用配置文件或代码在特定环境中快速启用或禁用调试。但是,疏忽或配置管理不当可能导致在非开发环境中启用调试,例如暂存或生产。在此配方中,我们将修复非开发环境中启用的调试功能。
准备好了吗
对于本章中的食谱,我们需要一个在线银行应用示例。
通过克隆 ASP.NET Secure Codeing Cookbook 存储库,打开命令 shell 并下载示例网上银行应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter07\debug-enabled\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
OnlineBankingApp\Startup.csand go to theConfigurestartup method. TheConfiguremethod makes a call toUseDeveloperExceptionPageeven if it is in a non-development environment:public void Configure(IApplicationBuilder appBuilder, IWebHostEnvironment environment) { if (environment.IsDevelopment()) { appBuilder.UseDeveloperExceptionPage(); } else { appBuilder.UseDeveloperExceptionPage(); // appBuilder.UseExceptionHandler("/Error"); appBuilder.UseHsts(); } // code removed for brevityUseDeveloperExceptionPage将生成一个包含SystemException详细信息的 HTML 页面。异常可以从堆栈跟踪及其显示的调试信息向攻击者提供重要信息,因此在暂存或生产环境中暴露这些信息并不理想。 -
We avoid the unnecessary exception details shown either in Staging or Production by removing the line of code that calls
UseDeveloperExceptionPageand uncommenting the line that invokesUseExceptionHandler:public void Configure(IApplicationBuilder appBuilder, IWebHostEnvironment environment) { if (environment.IsDevelopment()) { appBuilder.UseDeveloperExceptionPage(); } else { appBuilder.UseExceptionHandler("/Error"); appBuilder.UseHsts(); }对
UseExceptionHandler中间件进行调用将捕获异常并处理异常以返回更友好的OnlineBankingApp\Pages\Error.cshtml页面。
它是如何工作的…
调试对于 ASP.NET web 开发人员了解其应用的运行情况非常有用。在我们的示例网上银行 web 应用中,将对UseDeveloperExceptionPage方法的调用错放在代码中将允许在暂存或生产中显示异常详细信息。
我们通过将UseDeveloperExceptionPage方法置于使用environment.IsDevelopment方法的开发环境下是否运行 web 应用的条件检查下来修复代码。这可以防止在暂存或生产中进行调试和检查,从而避免向黑客暴露易受攻击的信息。
修复禁用的安全功能
添加防御和保护层有助于 ASP.NET Core web 应用免受攻击。Web 应用服务器具有内置的安全功能,例如配置为作为 HTTP 响应的一部分发送回客户端的安全头,指示浏览器启用安全机制。默认情况下,并不是所有这些安全头都是打开或添加的,因此在代码中启用它将由 web 开发人员掌握。
在此配方中,我们将添加缺少的 HTTP 安全头,以在我们的示例网上银行应用中启用保护。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 Visual Studio 代码,打开位于\Chapter07\disabled-security-features\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来修复此配方中缺少的安全功能。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Startup.csand look at theUsemethod call inConfigure. The middleware is adding theX-XSS-ProtectionHTTP security header but the value is set to0:app.Use(async (context, next) => { context.Response.Headers.Add("X-XSS-Protection", "0" ); await next(); });将
X-XSS-Protection响应头的值设置为0将指示浏览器禁用其 XSS 过滤及其对跨站点脚本(XSS)的保护。XSS 过滤器是一种浏览器安全功能,可保护用户免受跨站点脚本攻击。修复 XSS 漏洞将在第 8 章跨站点脚本中介绍。其他安全头,如
X-Content-Type-Options和X-Frame-Options也丢失了,这使得我们的示例网上银行应用缺乏针对攻击的保护。 -
To include these missing security features, we replace the old code with the following lines, which will protect our sample Online Banking web application:
app.Use(async (context, next) => { context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); context.Response.Headers .Add("X-Content-Type-Options", "nosniff"); context.Response.Headers .Add("X-Frame-Options", "DENY"); context.Response.Headers.Add("Referrer-Policy", "no-referrer"); await next(); });这些 HTTP 响应头中的每一个都有一个安全目的,这将在如何工作一节中解释。
笔记
虽然
X-XSS-Protection已经被弃用,但如果您预计用户仍将使用较旧的浏览器,那么它仍然是一个有用的 HTTP 头,可以在您的 web 应用中启用。另一种选择是实施内容安全策略,我们将在第 13 章、最佳实践中介绍。
正在验证安全标头
以下是步骤:
-
在菜单中导航至终端****新终端,或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/。* Log in using the following credentials:
a) 。电邮:
stanley.s.jobson@lobortis.cab) 。密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the Home page:
![Figure 7.1 – Home page authenticated]()
图 7.1–主页验证
- 按F12打开浏览器的开发者工具。* 进入网络页签并在流量列表中选择第一个 HTTP 流量。* 选择单个 HTTP 流量后,在右侧窗格中滚动以查看相应的 HTTP 响应安全标头:*
- 打开浏览器并转到
*
图 7.2–安全标头
安全标头现在作为从示例网上银行 web 应用发送的 HTTP 响应的一部分添加。
它是如何工作的…
当 HTTP 安全头被指定为 web 服务器或 web 应用发送的响应的一部分时,这将指示 web 浏览器启用对 XSS、clickjacking 和其他类型的 web 应用相关漏洞的保护。在前面的代码中,我们添加了以下 HTTP 安全标头:
-
X-XSS-Protection:当指令设置为1时,X-XSS-Protection安全头告诉浏览器启用 XSS 过滤器。当检测到 XSS 时,XSS 筛选器可以防止 XSS 并停止加载页面。 -
X-Content-Type-Options: TheX-Content-Type-Optionsheader, if assigned with anosniffdirective, prevents MIME sniffing. MIME-type sniffing is a browser behavior where it guesses what the MIME-type is of a resource of a page, but this behavior can be tricked into executing malicious content. This response header tells the browser to believe theContent-Typeheader's value and not attempt to guess the page's mime type implicitly.大多数动态应用安全测试工具通常将缺少
Content-Type响应头标记为漏洞发现。将X-Content-Type-Options: nosniff与明确声明的Content-Type响应头结合使用。所有主要浏览器都支持X-Content-Type-Options标题,如 Firefox、Chrome 和 Edge。 -
X-Frame-Options:X-Frame-Optionsis also an HTTP response header, which when set toDENYwill tell the browser to not allow the page to be rendered or embedded in any of the following HTML elements:<iframe>,<frame>,<embed>, or<object>. Malicious websites abuse these HTML elements to masquerade as authentic by embedding or framing the legitimate websites inside. Users are tricked in to clicking on the UI on what they presume rendered in front of the browser is a legit website. This attack is called Click-Jacking.提示:
若要允许您的一个网页在源代码相同的 web 应用中被框接,请将
X-Frame-Options标题的值指定为SAMEORIGIN:context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); -
Referrer-Policy:最后,为了防止 ASP.NET Core web 应用 URL 中的敏感信息在跨站点请求期间暴露,请将您的Referrer-Policy设置为no-referrer。Referrer头显示了用户请求发起的 URL,no-referrer值明确指示浏览器从 HTTP 头中删除Referrer。
其他值可能更适合您的 ASP.NET Core web 应用要求。请访问Mozilla 开发者网络中的推荐人政策文档,以获取中的替代值 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 。
禁用不必要的功能
大多数 ASP.NET Coreweb 应用功能都是有用的,但有些功能可能是不必要的,有时甚至是有害的。Web 开发人员必须考虑是否需要在代码中启用 Web 服务器或应用功能。我们需要删除一些功能以确保 ASP.NET Core web 应用的安全。
在此配方中,我们将删除ServerHTTP 头以防止 web 服务器信息泄露。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 Visual Studio 代码,打开位于\Chapter07\unnecessary-features\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来禁用此配方中不必要的功能。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Program.csand notice the value of one of the properties of theKestrelOptionsAddServerHeader:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webHost => { webHost.UseKestrel(kestrelOptions => { kestrelOptions.AddServerHeader = true; kestrelOptions.ConfigureHttpsDefaults (https => { https.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; }); }); webHost.UseStartup<Startup>(); });当此布尔属性设置为
true时,AddServerHeader属性在响应中添加ServerHTTP 头。此标头不是必需的,但会提供恶意参与者有关构建 web 应用的平台的信息。 -
We remove this unnecessary HTTP header by setting the value of the
AddServerHeaderproperty tofalse:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webHost => { webHost.UseKestrel(kestrelOptions => { kestrelOptions.AddServerHeader = false; kestrelOptions.ConfigureHttpsDefaults (https => { https.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; }); }); webHost.UseStartup<Startup>(); });当设置为
false时,ServerHTTP 头将不再作为响应发送给客户端,从而限制提供给坏参与者的信息。笔记
其他不需要的 HTTP 头包括X-Powered-By和X-AspNet-Version。这两个 HTTP 头都会泄露有关 web 主机的软件信息,这可能会使威胁参与者受益。
如果您在 IIS 中托管 ASP.NET Core web 应用,则默认情况下通常会显示这些标头,最好将其删除。要删除
X-Powered-By或X-AspNet-Version标题,请按照 Microsoft 官方文档中 IIS 参考的customheaders部分中的步骤进行操作 https://docs.microsoft.com/en-us/iis/configuration/system.webServer/httpProtocol/customHeaders/ 。
它是如何工作的…
默认情况下,Kestrel 是与我们的示例网上银行应用一起使用的 web 服务器,但Server头可用于任何 web 服务器类型。一些 web 服务器的默认实例中已经有了它,或者通过 web 应用的代码进行了配置。使用浏览器的开发工具,我们可以看到我们的 ASP.NET Core web 应用发送了一个服务器 HTTP 头,向我们提供了 web 服务器正在运行 Kestrel 的详细信息:

图 7.3–服务器 HTTP 头
在我们的方法中,我们禁用将此Server头作为响应发送回浏览器,从而限制恶意代理关于 web 应用运行在何种平台上的信息。了解这个细节可以让坏角色利用它可以针对应用执行的特定漏洞。
通过错误消息修复信息泄露
日志可以帮助确定在应用内部发生的事件。应用跟踪、调试信息、错误、警告和其他信息也可用,并写入日志中。不幸的是,在 ASP.NET Core web 应用中,敏感数据也可能被不小心地记录,无意中泄露会带来风险。
在此配方中,我们将更改配置错误的日志提供程序属性,以防止过于敏感而无法存储在日志中的日志信息。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 Visual Studio 代码,打开位于\Chapter07\information-exposure1\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤,通过错误消息修复信息泄露。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
appsettings.jsonand look at the currentLogLevelvalues:{ "Logging": { "LogLevel": { "Default": "Trace", "Microsoft": "Trace", "Microsoft.Hosting.Lifetime": "Trace" } }, "AllowedHosts": "*", "ConnectionStrings": { "OnlineBankingAppContext": "Data Source = OnlineBank.db" } }Trace值指定我们的示例网上银行 web 应用将向所有日志提供程序写入跟踪日志。该值为最低级别的,它指定还将记录任何更高级别的内容,包括敏感调试信息。 -
We fix the issue by setting these values to
Warning, which is levels higher thanTrace:{ "Logging": { "LogLevel": { "Default": "Warning", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "OnlineBankingAppContext": "Data Source=OnlineBank.db" } }为这些类别分配
Warning属性值将指示记录器写入由于意外事件而发生的错误日志,这些意外事件不会导致我们的示例网上银行 web 应用崩溃。
它是如何工作的
登录 ASP.NET Core web 应用在appsettings.json的Logging部分配置。在此文件中定义的每个日志类别(Default、Microsoft、Microsoft.Hosting.Lifetime)中,它们的值都设置为Trace,这是最低的LogLevel,值为0。此LogLevel不适合生产环境,这就是我们修改值并将其更改为Warning的原因。
通过不安全的 cookie 修复信息泄露
Cookie 对于在 ASP.NET Core web 应用中维护状态至关重要。敏感 cookie,如用于认证会话的 cookie,应仅通过 HTTPS 传输,并标记为HTTP-Only,以防止攻击者窃取存储在这些 cookie 中的信息。
在此配方中,我们将配置 cookie 策略,以防止 ASP.NET Core 示例 web 应用生成持久 cookie。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 Visual Studio 代码,打开位于\Chapter07\information-exposure2\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤,通过持久 cookie 配方修复信息暴露。
怎么做…
让我们来看看这个配方的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Startup.csand look at the following sections of code underConfigureServices:services.AddSession(options => { options.Cookie.Name = ".OnlineBanking.Session"; options.IdleTimeout = TimeSpan.FromSeconds(10); options.Cookie.HttpOnly = false; options.Cookie.SecurePolicy = CookieSecurePolicy.None; options.Cookie.IsEssential = true; });将
CookiePolicyOptions的SecurePolicy属性设置为枚举值CookieSecurePolicy.None,此配置表示我们示例应用中的会话 cookie 将不具有安全属性。此外,将会话 cookie 的
HttpOnly属性分配给false值,使得客户端脚本可以读取会话 cookie。 -
为了防止恶意的任意 JavaScript 代码读取我们会话 cookie 的值,我们将会话状态服务的
HttpOnly属性设置为true:services.AddSession(options => { options.Cookie.Name = ".OnlineBanking.Session"; options.IdleTimeout = TimeSpan.FromSeconds(10); options.Cookie.HttpOnly = true; -
To also ensure that the cookie policy middleware will mark the cookies of our sample Online Banking web application with a Secure attribute, we assign the
SecurePolicyproperty of theCookiePolicyOptionswithCookieSecurePolicy.Always:options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.IsEssential = true; });启用
HttpOnly属性将使用HttpOnly属性标记会话 cookie。
正在验证会话 cookie 属性
以下是步骤:
-
在菜单中导航至终端****新终端,或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/。* Log in using the following credentials:
a) 。电邮:
axl.l.torvalds@ut.netb) 。密码:
6GKqqtQQTii92ke!- Once authenticated, you will be redirected to the Home page:
![Figure 7.4 – Home page authenticated]()
图 7.4–主页验证
- 按F12打开浏览器的开发者工具。* Expand the Cookies tree on the left pane of the developer tools:
![Figure 7.5 – Cookies section]()
图 7.5–Cookies 部分
- 转到的应用选项卡(Firefox 中的存储选项卡),注意HttpOnly和安全属性的列被选中:*
- 打开浏览器并转到
*
图 7.6–会话 cookie
这表明在 cookie 中启用了Secure和HttpOnly属性。
它是如何工作的
HttpOnly和Secure是 Cookie 中最重要的两个属性,但都是可选的。这些 cookie 属性必须作为 cookie 策略中间件的一部分显式声明和配置。通过将HttpOnly属性设置为true,我们在会话 cookie 中追加HttpOnly属性:
options.Cookie.HttpOnly = true;
如果没有HttpOnly属性,任意客户端脚本可以读取可能包含敏感数据的 cookie 值。没有 JavaScript 代码能够从属性中检索值。
笔记
我们通常用HttpOnly属性标记的 Cookie 包含的值可能会受到会话劫持或跨站点脚本(XSS)等攻击的攻击。
通过在中间件服务管道中配置 cookie 策略,我们还将Szecure属性包括在应用 cookie 中:
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
CookieSecurePolicy.Always确保 cookie 仅通过HTTPS发送。**
八、跨站点脚本
跨站点脚本仍然是当今 web 应用中普遍存在的漏洞之一。也称为XSS,它是一种安全缺陷,允许攻击者将恶意客户端代码插入 ASP.NET Core 网页。由于缺乏净化和过滤,注入的输入成为可能,浏览器处理不需要的任意代码。
未知用户可以在浏览器中运行恶意脚本的 XSS 攻击中查看易受攻击的网页。一旦代码执行,攻击者可能会将用户重定向到恶意网站,可能会窃取其会话 cookie,或破坏您的 ASP.NET Core web 应用。
在本章中,我们将介绍以下配方:
- 固定反射 XSS
- 修复存储/持久 XSS
- 修复 domxss
在本章结束时,您将学习如何通过正确编码和转义输出来保护 ASP.NET Core web 应用不受不同类型 XSS 的影响。您还将通过使用第三方库函数来减轻跨站点脚本攻击,从而发现逃避输出的方法。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter08 。
固定反射 XSS
反射式跨站点脚本是一种 XSS 类型,在这种 XSS 中,坏角色可以将代码作为 HTTP 响应的一部分注入。反射的 XSS 是非持久性的,不会存储在数据库中,但攻击负载会返回到浏览器,反映不可信的输入。
当输出未编码时,可能会出现反射的 XSS 漏洞;当被欺骗的用户单击包含 XSS 负载的恶意链接时,可能会利用该漏洞进行攻击。在此配方中,我们将使用 Razor 页面的内置编码功能修复反射的 XSS 漏洞。
准备好了吗
对于本章中的菜谱,我们需要一个示例网上银行应用。
通过克隆 ASP.NET Secure Codeing Cookbook 存储库,打开命令 shell 并下载示例网上银行应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter08\reflected-xss\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
现在,让我们实际了解如何利用反射的 XSS 漏洞。
测试反射 XSS
以下是步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Loans。* Log in using the following credentials:
a) 电邮:
stanley.s.jobson@lobortis.cab) 密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the Loans page:
![Figure 8.1 – Loans page]()
图 8.1–贷款页面
- Type
Packtas the keyword to search in the textbox on top of the empty table and hit the Filter button. Notice that the search term is displayed as a message:
![Figure 8.2 – Keyword search]()
图 8.2–关键字搜索
- 为了测试我们的示例网银解决方案是否易受 XSS 攻击,我们在搜索栏中输入 XSS 有效负载
<script>alert("Pwned")</script>,然后点击过滤器按钮。检查此测试的结果:*
- 打开浏览器并转到
*
图 8.3–反射 XSS
请注意,显示了一个警报框,指示 XSS 注入成功,并证明贷款页面容易受到反射 XSS 的攻击。检查测试生成的 URL。攻击者可以通过电子邮件或社会工程策略将此 URL 发送给未知用户:https://localhost:5001/Loans?SearchString=%3Cscript%3Ealert%28%22Pwned%22%29%3C%2Fscript%3E。
怎么做…
让我们看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Pages\Loans\Index.cshtmland observe the markup just after the HTML table element:<table class="table"> @{ if(Model.SearchString is not null){ <h2>Your searched for @Html.Raw(@Model.SearchString) returned @Model.Loan.Count results</h2> } } // code removed for brevity输入的贷款票据搜索词使用
Html.Raw显示在页面上,允许在<h2>HTML 标记中呈现未过滤的字符串。通过在搜索文本框中输入恶意的跨站点脚本有效负载攻击,坏参与者可以利用缺少输出编码的漏洞进行攻击。 -
To remediate the security flaw in our code, we remove the call to the
Html.Rawmethod:<table class="table"> @{ if(Model.SearchString is not null){ <h2>Your searched for @Model.SearchString returned @Model.Loan.Count results</h2> } }Html.Raw不应用于呈现用户控制的输入。避免使用这种方法。 -
要测试代码修复是否工作,请在测试 XSS部分重复步骤 1-7并查看结果:

图 8.4-缓解的反射 XSS
请注意,反射的 XSS 负载不再工作。
它是如何工作的…
贷款页面提供了一个搜索功能,可以输入关键字来定位并返回匹配的记录。当使用Html.Raw方法显示搜索词时,此页面易受攻击。Html.Raw是一种返回未编码字符串的方法,将此页面暴露于反射的 XSS 攻击。
我们通过删除对Html.Raw方法的调用,并使用内置的 Razor 语法来呈现SearchString的标记,从而降低了这种风险并防止了攻击。计算为字符串类型的 Razor 语法返回一个转义字符串,该字符串使SearchString输出能够安全地呈现。
固定存储/持久 XSS
存储或持久 XSS是另一种类型的跨站点脚本漏洞。存储数据的 ASP.NET Core web 应用可能容易受到此 XSS 攻击变体的攻击。当攻击者提供的受污染数据保存在永久性存储或数据库中,并最终通过查看这些易受攻击的 ASP.NET Core 网页而不首先让应用输出转义数据而交付给用户时,就会发生存储 XSS。在此配方中,我们将通过在页面上显示数据时使用编码值修复存储的 XSS 漏洞。
让我们实际了解如何利用反射的 XSS 漏洞。
测试存储的 XSS
以下是步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Loans。* Log in using the following credentials:
a) 电邮:
axl.l.torvalds@ut.netb) 密码:
6GKqqtQQTii92ke!- 一旦认证,您将被重定向到贷款页面。请注意显示的消息:*
- 打开浏览器并转到
*
图 8.5–存储的 XSS
此时会显示一个警报框,指示贷款页面上的数据已被污染,从而使我们的示例网上银行应用易于存储 XSS。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 Visual Studio 代码,打开位于\Chapter08\stored-xss\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来修复此配方中缺少的安全功能。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Pages\Loans\Index.cshtmland examine the data cell that displaysNotein the<td>tag:@foreach (var item in Model.Loan) { <tr> <td> @Html.DisplayFor(modelItem => item.Amount) </td> <td> @Html.DisplayFor(modelItem => item.PeriodInMonths) </td> <td> @Html.DisplayFor(modelItem => item.TransactionDate) </td> <td> @(new HtmlString(item.Note)) </td> <td> <a asp-page="./Edit" asp-route- id="@item.ID">Approve Loan</a> | </td> </tr> }单元格中呈现的是来自使用
HtmlString类的数据库的Note数据,但默认情况下,HtmlString类的实例未编码,显示页面的输出将导致 XSS 漏洞。 -
To fix this stored XSS security bug, we use the
Valueproperty of theHtmlStringobject:@foreach (var item in Model.Loan) { <tr> <td> @Html.DisplayFor(modelItem => item.Amount) </td> <td> @Html.DisplayFor(modelItem => item.PeriodInMonths) </td> <td> @Html.DisplayFor(modelItem => item.TransactionDate) </td> <td> @(new HtmlString(item.Note).Value) </td> <td> <a asp-page="./Edit" asp-route- id="@item.ID">Approve Loan</a> | </td> </tr> }Value属性返回Note字段的 HTML 编码值,这使我们的贷款页面免受存储或持久的 XSS 攻击。 -
要测试代码修复是否工作,重复测试存储 XSS部分中的步骤 1-5并查看结果:

图 8.6–缓解的存储 XSS 漏洞
请注意,持久性 XSS 负载没有执行。
它是如何工作的…
我们的示例网上银行 web 应用在Models\SeedData.cs处植入了贷款数据。我们使用一个贷款申请填充 SQLite 数据库:
Loans = new List<Loan>{
new Loan {
Amount = 35000.00m,
TransactionDate = DateTime.Now,
PeriodInMonths = 24,
Note = "<script>alert('Pwned')</script>",
Status = LoanStatus.Pending
}
}
注意,Note属性被分配了一个持久的 XSS 负载。此滥用案例模拟了攻击者由于缺少验证而在数据库中保存受污染数据的场景。
加载贷款页面时,我们看到alertJavaScript 函数被执行,并弹出一个警报框。存储的 XSS 有效负载已成功运行,因为item.Note是使用HtmlString呈现的。默认情况下,HtmlString类返回一个未替换的字符串,这使得持久的 XSS 攻击成为可能。我们通过简单地利用HtmlString类的Value属性修复代码中的安全问题,该属性返回编码的字符串,并保护我们的 web 应用免受存储 XSS 攻击。
还有更多…
另一种降低风险的方法是使用HtmlEncoder类。正如我们从第 1 章安全编码基础使用 HtmlEncoder 配方的输出编码中了解到的,HtmlEncoder类有一个Encode方法,该方法可以转义传递给该方法参数的字符串。
与步骤 2和3类似,首先在Pages\Loans\Index.cshtmlRazor 页面标记顶部添加@inject指令,以注入HtmlEncoder服务:
@inject System.Text.Encodings.Web.HtmlEncoder htmlEncoder
然后,调用HtmlEncoder服务的Encode方法,通过item.Note:
<td>
@(htmlEncoder.Encode(item.Note))
</td>
Encode方法将逃逸item.Note的值,从而修复持久的 XSS 问题。
固定 DOM XSS
文档对象模型(DOM)是表示 HTML 页面的对象接口。此接口允许客户端脚本操作、添加或删除文档中的元素。与 JavaScript 编程语言结合使用的客户端脚本可能会被不安全地编写,并打开安全漏洞,例如基于 DOM 的 XSS。
与反射和存储的 XSS 相比,DOM XSS不是服务器端漏洞。缺点在于客户端代码试图修改 DOM 以显示数据,但由于缺乏编码和正确转义,将输入解释为代码。在此配方中,我们将使用 JavaScript 库中的编码函数修复基于 DOM 的 XSS 漏洞。
现在让我们实际了解如何测试 DOM XSS 漏洞。
测试 domxss
以下是步骤:
-
在菜单中导航至终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Loans。* Log in using the following credentials:
a) 电邮:
stanley.s.jobson@lobortis.cab) 密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the Loans page:
![Figure 8.7 – Loans page]()
图 8.7–贷款页面
- Type
caras the keyword to search in the textbox on top of the empty table and hit the Filter button. Notice that the search term is displayed as a message and a matching record is displayed:
![Figure 8.8 – Keyword search]()
图 8.8–关键字搜索
- 为了测试我们的示例网上银行解决方案是否易受基于 DOM 的 XSS 攻击,我们在搜索栏中输入 XSS 负载
<script>alert("Pwned")</script>,然后点击过滤器按钮。检查此测试的结果:*
- 打开浏览器并转到
*
图 8.9–DOM XSS
请注意,显示了一个警报框,指示 XSS 注入成功,并证明贷款页面易受 DOM XSS 攻击。检查测试生成的 URL。坏演员可以通过电子邮件或社会工程将此 URL 发送给未知用户。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 Visual Studio 代码,打开位于\Chapter08\dom-xss\before\OnlineBankingApp\的示例网上银行应用文件夹。
您可以执行此文件夹中的步骤来修复此配方中的 DOM XSS。
怎么做…
让我们看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Pages\Loans\Index.cshtmland notice the JavaScript code at the lower part of thecshtmlpage within@section Scripts:@section Scripts { @{ if(Model.SearchString is not null){ <script> $(document).ready(function () { var param = new URLSearchParams (window.location.search); var searchString = param.get ('SearchString'); var message = '<br><h2> You searched for ' + searchString + '</h2>'; $('#searchForm').append(message); }); </script> } } }动态附加到页面的是从 URL 的查询字符串检索到的帮助知识库搜索词。如果没有输出转义添加到页面文档对象模型中的不可信消息字符串,这种不安全的代码可能会导致基于 DOM 的 XSS。
在代码中解决此安全问题的一种方法是使用具有出色编码功能的 JavaScript 库。流行的 JavaScript 库之一是
underscore.js。 -
To start the remediation, we open
Areas\Identity\Pages\_ValidationScriptsPartial.cshtmland add a reference to theunderscore.jslibrary:<environment exclude="Development"> <script src="https://cdn.jsdelivr.net/npm/ underscore@1.12.0/underscore-min.js"></script> <script src=https://ajax.aspnetcdn.com/ajax/ jquery.validate/1.17.0/jquery.validate.min.js // code removed for brevity您可以拥有自己的
underscore.js库副本,也可以从内容交付网络(CDN添加参考。您在_ValidationScriptsPartial.cshtml中指定 URL,以使其功能在整个示例网上银行解决方案中可用。 -
We add a reference to
_ValidationScriptsPartial.cshtmland make a call to the_.escapefunction of theunderscore.jslibrary:@section Scripts { <partial name="_ValidationScriptsPartial" /> @{ if(Model.SearchString is not null){ <script> $(document).ready(function () { var param = new URLSearchParams (window.location.search); var searchString = param.get ('SearchString'); var message = '<br><h2> You searched for ' + _.escape (searchString) + '</h2>'; $('#searchForm').append(message); }); </script> } }调用
_.escape函数对searchString进行编码,并替换<、>、&、、等字符,这些字符可能存在潜在的恶意。**
*** 要测试代码修复是否工作,请重复测试 DOM XSS部分中的步骤 1-7并查看结果:**
**
图 8.10-固定的 DOM XSS
请注意,警报框不再显示,HTML 页面的 DOM 没有附加恶意的<script>标记。
它是如何工作的…
使用jQuery 低调的 AJAX 库,我们试图通过盲目地从window.location.search值中添加原始字符串来显示searchStringURL 查询字符串参数。最初的目的是显示用于搜索的关键字。然而,如果没有必要且适当的searchString编码,犯罪者可以制作有效载荷并将其分配给querystring参数。通过这个恶意链接,一个坏的参与者可能会欺骗用户点击它并无意中执行负载。
我们通过使用名为underscore.js的第三方库解决了该问题。underscore.js有很多有用的函数,其中一个可以转义字符串。._escape函数替换可能导致 DOM XSS 漏洞的字符,其 HTML 实体对应项将searchString转换为无害字符串。
一般来说,避免调用 JavaScript 方法,例如document.write,这些方法可以呈现未过滤和未编码的数据。坏角色可以通过动态操作 DOM 和执行任意客户端代码来利用此向量。*****
九、不安全反序列化
.NET 完全支持数据的序列化和反序列化。此语言功能允许 ASP.NET Core web 应用将内存中的对象转换为字节流(序列化),并将这些字节流重建回对象(反序列化)。序列化使数据的传输、存储和缓存以及系统之间的状态持久化成为可能。
在反序列化过程中,数据格式可以是JavaScript 对象表示法(JSON)或可扩展标记语言(XML),也可以是二进制格式。但是,与任何输入类型一样,数据源在作为内存对象反序列化回 web 应用之前可能不可信或被篡改。此漏洞通常称为不安全反序列化。使用过时且危险的反序列化程序、缺少数据验证和错误配置库是导致不安全反序列化攻击发生的一些根本原因。此安全漏洞可归因于不安全的代码,在最坏的情况下可能导致拒绝服务(DoS攻击和远程代码执行(RCE攻击)。
在本章中,我们将介绍以下配方:
- 修复不安全的反序列化
- 修复不安全反序列化程序的使用
- 修复不受信任的数据反序列化
在本章末尾,您将学习如何使用正确配置的库安全地反序列化输入,如何减轻过时的.NET 类给 ASP.NET Core web 应用带来的风险,以及如何使用更好的反序列化器替代方案来安全地反序列化数据流。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter09 。
修复不安全的反序列化
Json.NET一直以来都是.NET 开发人员处理 Json 的流行框架,直到.NET 最近在System.Text.Json名称空间下引入了自己的一组序列化器/反序列化器类。这组新类删除了.NETCore 以前版本对库的依赖关系。
Json.NET 具有类型处理功能,如果使用不当,ASP.NET Core web 应用容易受到不安全的反序列化攻击。自动类型处理将允许Json.NET流反序列化程序在传入请求中使用声明的.NET 类型。允许您的应用根据不受信任的源中声明的.NET 类型自动反序列化对象可能是有害的,并且可能会导致意外对象的实例化,从而导致在主机中执行任意代码。在此配方中,我们将修复此不安全的反序列化,并防止有害的自动类型处理。
准备好了吗
对于本章的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆ASP.NET-Core-Secure-Coding-Cookbook存储库下载示例网银应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter09\unsafe-deserialization\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
让我们看看如何使用工具来发现不安全的反序列化漏洞。
测试不安全的反序列化
为了搜索代码漏洞,我们可以使用代码分析器或 linter 等工具执行静态应用安全测试(SAST)。这个方法将使用开源的DevSkimVS 代码扩展来搜索代码中的安全缺陷。我们首先安装插件并查看结果:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
只需按Ctrl+Shift+X或点击 VS 代码左侧面板活动栏中的扩展图标,即可打开扩展视图。
-
In the Search box, type in
devskimto bring up the DevSkim extension, as shown in the following screenshot:![Figure 9.1 – DevSkim VS Code extension]()
图 9.1–DevSkim 与代码扩展
-
Click Install to install the DevSkim VS Code extension, as shown next:
![Figure 9.2 – Installation]()
图 9.2–安装
-
Once installed, let's configure the extension settings by clicking the gear icon in the DevSkim view and selecting Extension Settings:
![Figure 9.3 – Extension Settings option]()
图 9.3–扩展设置选项
-
Enable these DevSkim settings, as shown in the following screenshot:
![Figure 9.4 – Enabling optional settings]()
图 9.4–启用可选设置
这两个设置将启用提醒 ASP.NET Core 开发人员编写安全代码的最佳实践的规则:
a) 启用最佳实践规则
b) 启用手动审核规则
-
Open
Pages\Loans\Upload.cshtml.csand examine the code underlined with a squiggly line in one of the lines of code under theOnPostAsyncmethod, indicating that a secure coding rule has been triggered, as shown in the following screenshot:![Figure 9.5 – Warning]()
图 9.5–警告
-
按Ctrl+Shift+M在问题选项卡中查看发现的详细信息,或将鼠标悬停在曲线上了解更多关于安全性发现的信息,如下图所示:

图 9.6–DevSkim 修复指南
修复指南将建议您不要反序列化不受信任的数据,而是使用TypeNameHandling.None。
笔记
Visual Studio集成开发环境(IDE的全功能版本)具有内置的代码分析,其安全规则与 DevSkim 插件工具的规则相匹配:
CA2300:不要使用不安全的反序列化程序二进制格式化程序
CA2301:不调用 BinaryFormatter.Deserialize 而不首先设置 BinaryFormatter.Binder
CA2302:确保在调用 BinaryFormatter.Deserialize之前设置 BinaryFormatter.Binder
要了解关于不同 VS 代码分析安全规则的更多信息,请参见上.NET 基础官方文档中的安全规则https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings 。
怎么做…
让我们来看看这个食谱的步骤:
-
打开
Pages\Loans\Upload.cshtml.cs并在OnPostAsync方法中定位易受攻击的代码,正如 DevSkim 扩展using (var reader = new StreamReader (Upload.OpenReadStream())) { string fileContent = reader.ReadToEnd (); emptyLoan = (Loan) Newtonsoft.Json.JsonConvert .DeserializeObject(fileContent, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); }所指出的
-
Change the
TypeNameHandlingproperty toTypeNameHandling.None:using (var reader = new StreamReader (Upload.OpenReadStream())) { string fileContent = reader.ReadToEnd (); emptyLoan = (Loan) Newtonsoft.Json.JsonConvert .DeserializeObject(fileContent, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.None }); }请注意,在以下屏幕截图中,DevSkim VS 代码扩展不再标记代码以供查看:

图 9.7-已纠正的不安全反序列化
更改属性不再将TypeNameHandling属性标记为安全代码检查。
它是如何工作的…
JSON是标准数据格式,ASP.NET Core web 开发人员使用JSON.NET等框架来处理反序列化此数据格式的任务也就不足为奇了。大多数库都具有有助于反序列化的功能(如自动类型处理),但这些功能可能会引起安全问题。在这个配方中,我们已经看到,在将JsonSerializerSettings设置为TypeNameHandling.All的情况下启用类型处理会从我们的DevSkim工具中引发一个不反序列化不可信数据安全规则。在序列化过程中,分配除TypeNameHandling.None之外的TypeNameHandling属性包括.NET 类型名称,这将使我们的示例网上银行 web 应用面临不安全的反序列化攻击。我们通过简单地将TypeNameHandling属性设置为TypeNameHandling.None来修复代码中的此安全漏洞,从而阻止自动.NET 类型解析。
提示
确保您使用的是最新版本的序列化程序/反序列化程序库。旧版本可能有一个众所周知的漏洞,威胁参与者可能会利用该漏洞进行攻击。
记录反序列化异常和失败也很重要。当传入类型(Loan)不是使用强类型对象的预期类型时引发异常。
这里是一个修改后的代码片段,我们使用强类型对象,实现正确的日志记录(更多信息参见第 11 章、日志记录和监控不足),并执行异常处理(更多信息参见第 13 章、最佳实践:
try {
emptyLoan = (Loan) Newtonsoft.Json.JsonConvert .DeserializeObject<Loan>(fileContent, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None
});
}
catch (JsonException je) {
_logger.LogError($"Unexpected error deserializing data '{je.Message}'.");
throw new JsonException(je.Message);
}
还有更多…
商业 SAST 供应商为测试代码的安全性提供了企业级解决方案。这些解决方案要么托管在本地,要么基于云。它们允许用户和开发人员运行扫描并生成报告,按严重性和类别列出不同的漏洞。这些报告提供了修复方法和修复解决方案中发现的安全漏洞的方法,并可以指向发现问题的代码行。SAST 解决方案还显示数据流图(DFGs),供用户了解漏洞如何从源传播到接收器。
要了解更多关于 SAST 的,请访问开放式 Web 应用安全项目(OWASP)源代码分析工具文档。此 OWASP 参考资料将提供有关您的 SAST 选择过程的提示,以确定哪种解决方案适合您的组织,可在以下链接中找到:https://owasp.org/www-community/Source_Code_Analysis_Tools 。
修复不安全反序列化程序的使用
BinaryFormatter是 ASP.NET 开发人员可以用来序列化和反序列化数据的类型之一。微软官方的BinaryFormatter 安全指南文档中有一条关于将BinaryFormatter用作反序列化程序的严格警告。BinaryFormatter是一个不安全的类型,无法使用,因为此反序列化程序未检查其反序列化的类型。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码打开\Chapter09\insecure-deserializer\before\OnlineBankingApp\处的样本OnlineBankingApp文件夹。
您可以执行此文件夹中的步骤来修复不安全反序列化程序的使用。
怎么做…
让我们来看看这个食谱的步骤。
-
打开
Pages\Loans\Upload.cshtml.cs并检查OnPostAsync方法中利用危险的BinaryFormatter类反序列化FileStream:public async Task OnPostAsync() { Loan emptyLoan = null; var file = Path.Combine(_environment .ContentRootPath, "uploads", Upload.FileName); using (var fileStream = new FileStream(file, FileMode.Create)) { await Upload.CopyToAsync(fileStream); BinaryFormatter formatter = new BinaryFormatter(); fileStream.Position = 0; emptyLoan = (Loan) formatter.Deserialize (fileStream); }的代码
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** Type the following command in the terminal to build and run the sample app:dotnet build请注意,生成成功,但出现警告:
warning SYSLIB0011: 'BinaryFormatter.Deserialize(Stream)' is obsolete: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.BinaryFormatter不仅过时,而且不安全。强烈建议避免使用此类,并使用更安全的序列化程序/反序列化程序。- To remediate the risk, we must use a
SerializationBinderclass to validate the type that is being deserialized. We begin by adding a new file under theModelsfolder by pressing Ctrl + N and name itLoanDeserializationBinder.cs:
![Figure 9.8 – LoanDeserializationBinder.cs]()
图 9.8–LoanDeserializationBinder.cs
- 定义一个继承自
SerializationBinder的LoanDeserializationBinder类,并添加以下代码:
using System; using System.Runtime.Serialization; namespace OnlineBankingApp.Models { public class LoanDeserializationBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if (typeName.Equals ("OnlineBankingApp.Models.Loan")){ return typeof(Loan); } return null; } } }- Next, we refactor the code that uses
BinaryFormatter, assigning itsBinderproperty with the instance of theLoanDeserializationBinderclass:
using (var fileStream = new FileStream(file, FileMode.Create)) { await Upload.CopyToAsync(fileStream); BinaryFormatter formatter = new BinaryFormatter(); formatter.Binder = new LoanDeserializationBinder(); fileStream.Position = 0; emptyLoan = (Loan) formatter.Deserialize (fileStream); }通过使用
LoanDeserializationBinder类,我们可以检查被反序列化的类型,防止我们的示例网上银行 web 应用被利用。笔记
由于项目文件中启用了设置,此示例网上银行 web 应用能够使用
BinaryFormatter。在
OnlineBankingApp.csproj文件中有一个基于布尔值的EnableUnsafeBinaryFormatterSerialization节点,允许您的应用使用不安全的BinaryFormatter类:<PropertyGroup><TargetFramework>net5.0</TargetFramework><EnableUnsafeBinaryFormatterSerialization>true </EnableUnsafeBinaryFormatterSerialization><UserSecretsId>4869bcd3-3dab-4dae-a167-31816b317c8b</UserSecretsId></PropertyGroup>强烈建议避免启用此设置。*
- To remediate the risk, we must use a
*## 它是如何工作的…
如果没有Binder属性集,BinaryFormatter可能对我们的示例网上银行 web 应用有害。就其本身而言,BinaryFormatter接受传入的类型,没有验证。为了解决我们的代码安全问题,我们定义了一个从SerializationBinder类继承的新类,并将其分配给BinaryFormatter实例的Binder 属性:
formatter.Binder = new LoanDeserializationBinder();
LoanDeserializationBinder类检查类型并确保返回预期的Loan对象:
if (typeName.Equals("OnlineBankingApp.Models.Loan")){
return typeof(Loan);
}
然而,尽管这种方法降低了不需要的数据反序列化的风险,但它并不能完全防止其他类型的攻击。
还有更多…
评估您的 ASP.NET Core web 应用的风险状况对于确定BinaryFormatter是否适合此工作至关重要。建议避免使用这种类型,并选择更安全的替代品。根据要处理的数据,诸如DataContractSerializer、XmlSerializer、BinaryReader、BinaryWriter-甚至System.Text.Json名称空间下的类等安全选项都是更好的选择。
例如,为了在代码中使用DataContractSerializer类,我们将名称空间引用从System.Runtime.Serialization.Formatters.Binary更改为:
using System.Runtime.Serialization;
然后我们移除BinaryFormatter以将其替换为DataContractSerializer类:
using (var fileStream = new FileStream(file, FileMode.Create))
{
await Upload.CopyToAsync(fileStream);
var safeDeserializer = new DataContractSerializer (typeof(OnlineBankingApp.Models.Loan));
fileStream.Position = 0;
emptyLoan = (OnlineBankingApp.Models.Loan) safeDeserializer.ReadObject(fileStream);
}
使用时,DataContractSerializer类提供自动类型检查,使得 DoS 和 RCE 等攻击更难成功执行。
修复不可信数据反序列化
缺少类型检查并不是反序列化时需要注意的唯一事项。必须验证数据本身的完整性。
让我们看看不可信数据反序列化如何利用我们的示例网上银行 web 应用。
测试不可信数据反序列化
为了测试我们的样本网上银行 web 应用是否容易受到不可信数据反序列化的攻击,我们遵循以下步骤并使用受污染的文件:
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Loans/Upload。* Log in using the following credentials:
a) 电邮:
axl.l.torvalds@ut.netb) 密码:
6GKqqtQQTii92ke!- Once authenticated, you will be redirected to the Upload Loan Application page. This page will allow a loan officer to process a loan application by uploading a loan binary file:
![Figure 9.9 – Upload Loan Application page]()
图 9.9–上传贷款申请页面
- Start uploading a file by clicking Choose File, browse to the current directory, and select
file.dat. Hit Submit to upload thefile.datfile:
![Figure 9.10 – file.dat]()
图 9.10–file.dat
让我们看看当我们上传一个假定可信的文件时会发生什么。
- Open the DB Browser for SQLite (DB4S) tool and select the
OnlineBank.dbfile to view the loan record:
![Figure 9.11 – DB4S]()
图 9.11–DB4S
- 要查看
Loan表下的记录,请进入浏览数据页签,在表下拉列表中选择借款,如下图所示:*
- 打开浏览器并转到
*
图 9.12-篡改贷款申请
请注意,状态设置立即处于批准状态,因为上传的文件被篡改。由于代码中缺少数据验证,因此存在此安全缺陷。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码打开\Chapter09\deserialized-untrusted-data\before\OnlineBankingApp\处的样本OnlineBankingApp文件夹。
您可以执行此文件夹中的步骤来修复不受信任的数据反序列化。
怎么做…
让我们看看这个食谱的步骤:
-
打开
Pages\Loans\Upload.cshtml.cs并检查OnPostAsync方法中的代码。请注意,未执行任何数据检查或验证,将贷款状态设置为其初始预期值LoanStatus.Pending:using (var fileStream = new FileStream(file, FileMode.Create)) { await Upload.CopyToAsync(fileStream); BinaryFormatter formatter = new BinaryFormatter(); formatter.Binder = new LoanDeserializationBinder(); fileStream.Position = 0; emptyLoan = (Loan) formatter.Deserialize(fileStream); } var loggedInUser = HttpContext.User; var customerId = loggedInUser.Claims.FirstOrDefault (x => x.Type == ClaimTypes.NameIdentifier).Value; emptyLoan.CustomerID = customerId; emptyLoan.TransactionDate = DateTime.Now; if (await TryUpdateModelAsync<Loan>( emptyLoan, "loan", l => l.ID, l => l.CustomerID, l => l.Amount, l => l.PeriodInMonths, l => l.TransactionDate, l => l.Note)) { _context.Loan.Add(emptyLoan); await _context.SaveChangesAsync(); } -
To fix this security bug, we make sure that the business rules are followed and the state of important properties such as
LoanStatusis initialized:var loggedInUser = HttpContext.User; var customerId = loggedInUser.Claims.FirstOrDefault (x => x.Type == ClaimTypes.NameIdentifier).Value; emptyLoan.CustomerID = customerId; emptyLoan.TransactionDate = DateTime.Now; emptyLoan.Status = LoanStatus.Pending;通过前面的代码添加,我们可以防止反序列化过程中可能的数据篡改。
它是如何工作的…
数据验证检查阻止攻击者滥用我们的样本网上银行 web 应用。在检查代码的安全缺陷时,我们必须了解每个 ASP.NET Core 网页背后的业务规则。在前面的配方中,我们执行了一项测试,并查看了存储数据的完整性。我们发现file.dat文件被篡改,提交该文件导致贷款申请被自动批准。缺少安全控制,我们盲目地反序列化了文件,没有进行任何完整性检查。我们通过正确地初始化具有初始值的Loan对象,将Loan.Status属性设置为LoanStatus.Pending,解决了这个问题。**
十、使用已知漏洞的组件
ASP.NET Core web 开发人员依靠第三方商业和开源框架、库和包来构建 web 应用。这种方法加快了开发时间,以支持快速的业务需求。虽然这为开发人员节省了大量时间,但使用外部开发的组件存在风险。这些库中的代码安全性通常得不到保证,而且与其他任何软件一样,都会存在安全缺陷。有必要进行软件组成分析(SCA),以确定您的 ASP.NET Core web 应用是否使用了过时且易受攻击的软件包。
在本章中,我们将介绍以下配方:
- 修复易受攻击的第三方 JavaScript 库的使用
- 修复易受攻击的
NuGet包的使用 - 修复从不受信任的源托管的库的使用
在本章结束时,您将学习如何使用浏览器加载项和命令行工具查找易受攻击的库版本,如何将框架更新为更安全的NuGet包版本,如何确定不受信任的源是什么,以及如何采取步骤补救这些源在代码中引入的风险。
技术要求
这本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter10 。
修复易受攻击的第三方 JavaScript 库的使用
Web 开发离不开 JavaScript 库,因为它们帮助开发人员执行文档对象模型(DOM)操作并在网页中处理异步 JavaScript 和 XML(AJAX)。jQuery 就是这样一个库。尽管 jQuery JavaScript 库的效率很高,但与以前版本的 jQuery 库相关联的还有许多常见漏洞和暴露(CVEs。CVE 是公开的漏洞,详细说明了特定软件或组件的弱点。让我们看看如何使用浏览器扩展来发现易受攻击的 jQuery 版本。
准备好了吗
对于本章中的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆ASP.NET-Core-Secure-Coding-Cookbook存储库下载示例网银应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter10\vulnerable-jquery1\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
测试过时且易受攻击的第三方库
要确定应用是否使用易受攻击的 JavaScript 组件,我们可以使用Retire.js等工具。以下步骤将指导我们如何安装Retire.js并使用它在我们的示例网上银行 web 应用中测试过时且易受攻击的第三方库:
-
Using your preferred browser, install the
Retire.jsbrowser extension. The steps to installRetire.jsare more or less the same for most browsers.a) 使用 Chrome,在地址栏中键入以下内容以打开Chrome 网络商店:
https://chrome.google.com/webstore/category/extensions?hl=en-US&authuser=1
b) 一旦Chrome 网络商店加载,您将在页面左侧看到一个搜索栏。键入
Retire.js并点击进入:![Figure 10.1 – Retire.js in Chrome Web Store]()
图 10.1–Chrome Web Store 中的 Retire.js
c) 从搜索结果中选择
retire.js。您现在将被重定向到Retire.js扩展页面,在那里您可以安装扩展。点击添加到 Chrome按钮开始安装:![Figure 10.2 – Retire.js extensions page]()
图 10.2–Retire.js 扩展页面
d) 安装后,会弹出消息,通知您retire.js 已添加到 Chrome中:
![Figure 10.3 – Retire.js successful installation]()
图 10.3–Retire.js 成功安装
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Loans。* Log in using the following credentials:
a) 电邮:
stanley.s.jobson@lobortis.cab) 密码:
rUj5jtV8jrTyHnx!- 一旦认证,您将被重定向到贷款页面。* Notice that the
retire.jsplugin is loaded in the toolbar and is marked with a red exclamation point. Clicking it will show you details of the warning:
![Figure 10.4 – Retire.js in action]()
图 10.4–Retire.js 正在运行
retire.js插件将显示一个弹出窗口,显示 CVE 信息。我们的网上银行解决方案使用的是jQuery version 3.4.1,已知该漏洞具有中等严重性。*
- 打开浏览器并转到
*在此配方中,我们将使用库的最新版本修复易受攻击的第三方组件。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open
Pages\Shared\_Layout.cshtmland notice the script tag referencing an externaljquery-3.4.1.min.jsJavaScript file:<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>此 jQuery 版本易受跨站点脚本(XSS攻击)。
-
为了消除这种安全风险,我们将 jQuery 脚本引用更新为 jQuery 团队建议的最新版本,即升级到 3.5.0 版(或使用最新版本)。
-
在浏览器中,转至https://code.jquery.com/jquery/ 。
-
Scroll down until you find the latest version of jQuery Core (as of this writing, it is jQuery Core 3.6.0):
![Figure 10.5 – jQuery CDN references]()
图 10.5–jQuery CDN 参考
-
点击缩小链接,显示 jQuery 3.6.0 缩小版的代码集成弹出窗口,然后点击复制到剪贴板图标,将整个脚本标记引用复制到 jQuery CDN。
-
一旦我们有了对 jQuery 最新版本的 CDN 脚本引用,我们就通过替换整个易受攻击的脚本元素
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>来更新我们的
_Layout.html文件 -
要测试代码修复是否有效,请重复测试过期易受攻击的第三方库部分中的步骤 3-9并查看结果:

图 10.6-未发现任何漏洞
请注意,Retire.js插件不再显示页面易受攻击。
它是如何工作的…
Retire.js是一个有用的免费开源浏览器插件,它可以扫描网页以查找易受攻击的 JavaScript 库。Retire.js对页面中加载的资源执行被动扫描,并基于统一资源定位器(URL)、文件名、文件内容或哈希识别易受攻击的 JavaScript 库。
我们安装此浏览器加载项是为了识别我们的贷款页面易受基于CVE-2020-11022的 XSS 攻击。我们通过将 jQuery 库更新到最新版本来弥补 XSS 被利用的风险。
还有更多…
让我们使用Retire.js浏览器插件发现并修复另一个漏洞:
-
打开
\Chapter10\jquery2\before\OnlineBankingApp\处的样本OnlineBankingApp文件夹。 -
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/Loans。* Log in using the following credentials:
a) 电邮:
axl.l.torvalds@ut.netb) 密码:
6GKqqtQQTii92ke!- 一旦认证,您将被重定向到贷款页面。* 请注意,
Retire.js浏览器加载项的图标在工具栏中有一个感叹号。* Click theRetire.jsicon and the extension will display a pop-up window that shows the vulnerability information:
![Figure 10.7 – Scan result]()
图 10.7–扫描结果
附加组件表示在 jQuery 库中至少发现了四个中度严重性的 CVE。
- Click one of the hyperlinks beside each CVE to see details of the vulnerabilities. One of the items in the list shows that the page has a vulnerable jQuery version based on
CVE-2015-9251,CVE-2019-11358,CVE-2020-11022, andCVE-2020-11023, all of which are XSS vulnerabilities.
提示
您的应用可能受 CVE 的直接影响,也可能不受 CVE 的直接影响。例如,
2019-11358影响 Drupal 和内容管理系统(CMSes)。但是,即使您的 ASP.NET Core web 应用没有受到直接影响,纠正风险仍然是一个好主意。- You can hit the Save button to save the information into a page. A window will appear, asking if you want to view it in a browser:
![Figure 10.8 – Saving as a file]()
图 10.8–另存为文件
- The rendered page displays the same information as what is shown in the browser add-on:
![Figure 10.9 – Saved report]()
图 10.9–保存的报告
为了解决这个安全问题,我们还需要通过更新 CDN 引用升级到 jQuery 的最新版本。
- 打开
Pages\Shared\_Layout.cshtml并注意引用外部jquery-3.0.0-rc1.jsJavaScript 文件的script标记:
<script src="https://code.jquery.com/jquery-3.0.0-rc1.js"></script>- 重复本配方如何做部分的步骤 4-6。* Replace the reference to
jquery-3.0.0-rc1.jswith the content from the clipboard:
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>在启用扩展时重建项目并再次加载页面后,加载项将显示未发现任何漏洞:*
- 打开浏览器并转到
*
图 10.10–未发现任何漏洞
笔记
与任何软件一样,浏览器扩展也可能存在漏洞。使用浏览器加载项时要小心,并随时了解其最新版本和更新。
另见
您可以利用国家标准与技术研究所的(NIST 的)国家漏洞数据库(NVD)数据源和应用编程接口(API)。
NVD 保存所有已知漏洞的数据集合。其 API 的文档可在以下链接中找到:https://nvd.nist.gov/vuln/data-feeds#APIS 。
修复易受攻击的 NuGet 软件包的使用
库和组件可以从软件包管理器安装和使用,例如NuGet.VS Code 具有本机支持,这简化了 ASP.NET Core web 开发人员的安装过程。这样一来,它很快就引入了安装和使用易受攻击的NuGet 软件包的风险。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码打开\Chapter10\vulnerable-package\before\OnlineBankingApp\处的样本OnlineBankingApp文件夹。
让我们看看如何使用工具在我们的应用中发现易受攻击的NuGet软件包。
测试易受攻击的 NuGet 软件包
要确定您的应用是否使用易受攻击的 NuGet 软件包,我们可以使用另一个工具,如Dotnet Retire。首先,我们在我们的示例网上银行应用中安装dotnet retire漏洞扫描程序:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** Type the following command in the terminal to installdotnet-retire:dotnet tool install -g dotnet-retire成功安装后,您将看到以下消息:
You can invoke the tool using the following command: dotnet-retire Tool 'dotnet-retire' (version '5.0.0') was successfully installed.- Next, execute the tool by typing the following command:
dotnet-retire请注意扫描的结果:
info: RetireNet.Packages.Tool.Services.RetireLogger[0] Scan starting info: RetireNet.Packages.Tool.Services.RetireLogger[0] Analyzing 'OnlineBankingApp' fail: RetireNet.Packages.Tool.Services.RetireLogger[0] Found use of 1 vulnerable libs in 2 dependency paths. * Microsoft Security Advisory 4021279: Vulnerabilities in.NET Core, ASP.NET Core Could Allow Elevation of Privilege in System.Net.Http/4.3.1 https://github.com/dotnet/corefx/issues/19535 info: RetireNet.Packages.Tool.Services.RetireLogger[0] Scan complete.扫描结果表明存在一个易受攻击的库,即
System.Net.Http包的version 4.3.1。*
*您可以执行此文件夹中的步骤来修复易受攻击的 NuGet 软件包。
怎么做…
让我们来看看这个食谱的步骤。
-
在启动练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open
OnlineBankingApp.csproj. Notice that one of the packages referenced is the vulnerable version ofSystem.Net.Http:<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.1" /> <PackageReference Include="SendGrid" Version="9.22.0" /> <PackageReference Include="System.Net.Http" Version="4.3.1" /> <PackageReference Include="System.Text.Encodings.Web" Version="5.0.1" />为了修复此安全漏洞,我们将 NuGet 包升级到最新版本。
-
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以安装最新版本的System.Net.Http软件包:dotnet add package System.Net.Http- 安装成功后,您会看到版本在
OnlineBankingApp.csproj:
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.1" /> <PackageReference Include="SendGrid" Version="9.22.0" /> <PackageReference Include="System.Net.Http" Version="4.3.4" /> <PackageReference Include="System.Text.Encodings.Web" Version="5.0.1" />中更新* 在
dotnet add package命令中省略--version或-v将安装最新的 NuGet 软件包。* To test if the code fix worked, repeat Step 4 in the Testing vulnerable NuGet packages section and see the result:info: RetireNet.Packages.Tool.Services.RetireLogger[0] Scan starting info: RetireNet.Packages.Tool.Services.RetireLogger[0] Analyzing 'OnlineBankingApp' info: RetireNet.Packages.Tool.Services.RetireLogger[0] Found no usages of vulnerable libs! info: RetireNet.Packages.Tool.Services.RetireLogger[0] Scan complete.观察结果。
dotnet retire工具未发现任何易受攻击的软件包。* - 安装成功后,您会看到版本在
*## 它是如何工作的…
dotnet retire是一个命令行工具,帮助开发人员理解 ASP.NET Core 应用的依赖关系。这些依赖项通常都有我们都应该知道的漏洞,因此有必要执行安全扫描。
dotnet retire中的安全扫描搜索引用的易受攻击的 NuGet 库,以防止我们的示例网上银行 web 应用被公开的漏洞利用。
修复从不可信源托管的库的使用
我们使用的库和组件的来源必须来自安全和可信来源。这些库的主机大部分时间托管在 CDN 中,也可能受到攻击和滥用。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。使用 VS 代码打开\Chapter10\untrusted-source\before\OnlineBankingApp\处的样本OnlineBankingApp文件夹。
您可以执行此文件夹中的步骤来修复从不受信任的源托管的包的使用。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open
Pages\Loans\Index.cshtmland examine thescriptreference below the markup:<script src="http://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script src="~/lib/bootstrap/dist/js /bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>注意,虽然 jQuery 版本是最新版本,但协议是超文本传输协议(HTTP。
-
只需将协议从
http更改为https:<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script src="~/lib/bootstrap/dist/js /bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>
将协议从http更改为https可以确保对 CDN 中 JavaScript 库的引用是安全的,并且不会出现不安全传输带来的弱点。
提示
除了通过不安全的 HTTP 提供资源的主机之外,我们还必须注意利用托管在隐蔽和鲜为人知的域中的 JavaScript 库。
它是如何工作的…
ASP.NET Core web 开发人员必须确保库来自正确的来源,在我们的案例中,我们使用的是来自code.jquery.com的官方 CDN,该 CDN 是可信的。开发人员还必须通过与HTTP secure(HTTPS的安全链接添加对 jQuery 库(或任何 JavaScript 库)的引用:
https://code.jquery.com/jquery-3.6.0.min.js
还有更多…
还有主机和 CDN 被破坏的风险,因此为了验证资源的完整性,开发人员必须使用子资源完整性(SRI安全功能。浏览器中的 SRI 为 ASP.NET Core web 应用获取的资源提供完整性检查。它允许开发人员根据 web 应用试图从主机获取的资源的哈希值,传递从原始且未经缓冲的资源生成的哈希值:
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej /m4=" crossorigin="anonymous"></script>
大多数 CDN 为您提供散列,但您可以使用命令行工具生成自己的散列,也可以通过在线工具生成散列,如SRI 散列生成器(https://www.srihash.org/ )。
要了解更多有关 SRI 的详细信息,请阅读有关 SRI 的Mozilla 开发者网络(MDN)文档,可在找到 https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity 。****
十一、记录和监控不足
对 ASP.NET Core web 应用的攻击可能随时发生。开发人员必须授权其安全团队通过从 web 应用生成足够的日志来重构事件。记录正确的信息将有助于确定事件的详细信息,并为审计目的识别关键数据。未能记录关键安全信息的缺点是,安全团队无法生成正确的分析或报告。但是,过多的日志记录可能会导致敏感数据泄露。只有通过积极监测,才能对此类安全事件采取必要和立即的应对措施。开发人员必须在 ASP.NET Core web 应用生成的日志中启用监视,以实现更实时的防御。
在本章中,我们将介绍以下配方:
- 修复异常记录不足的问题
- 修复了数据库(DB事务记录不足的问题
- 修复过多的信息记录
- 修复缺乏安全监控的问题
在本章结束时,您将了解如何在我们的示例网上银行应用中正确添加适当的异常记录,如何记录关键 DB 事务,如何防止记录过多的数据或信息,以及如何启用安全监控。
技术要求
这本书的编写和设计是为了使用Visual Studio 代码(VS 代码)、Git 和.NET Core 5.0。配方中的代码示例主要在 ASP.NET Core Razor 页面中提供。示例解决方案还使用 SQLite 作为 DB 引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter11 。
修复异常记录不足的问题
安全相关事件,如用户身份验证或启用和禁用双因素身份验证(2FA)-当发生时,必须记录并跟踪。为了了解安全事件发生时的事件顺序,这些事件对于审核是必不可少的。
在此配方中,我们将利用 ASP.NET Core 的内置日志提供程序修复安全相关异常的日志记录不足问题。
准备好了吗
对于本章的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆ASP.NET-Core-Secure-Coding-Cookbook存储库下载示例网银应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter11\insufficient-logging-exception\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
打开
Areas\Identity\Pages\Account\Manage\Disable2fa.cshtml.cs并注意OnGetpublic async Task<IActionResult> OnGet() { var customer = await _customerManager.GetUserAsync(User); if (customer== null) { return NotFound($"Unable to load customer with ID '{_ customerManager.GetUserId(User)}'."); } if (!await _customerManager .GetTwoFactorEnabledAsync(customer)) { throw new InvalidOperationException($"Cannot disable 2FA for customer with ID '{_customerManager.GetUserId(User)}' as it's not currently enabled."); } return Page(); }下的代码
-
Also, notice the code under the
OnPostAsyncmethod:public async Task<IActionResult> OnPostAsync() { var customer = await _customerManager.GetUserAsync(User); if (customer == null) { return NotFound($"Unable to load customer with ID '{_customerManager.GetUserId(User)}'."); } var disable2faResult = await _customerManager.SetTwoFactorEnabledAsync (customer, false); if (!disable2faResult.Succeeded) { throw new InvalidOperationException ($"Unexpected error occurred disabling 2FA for customer with ID '{_customerManager .GetUserId (User)}'."); } _logger.LogInformation("Customer with ID '{UserId}' has disabled 2fa.", _customerManager.GetUserId(User)); StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; return RedirectToPage("./TwoFactorAuthentication"); }这两种方法都有一行代码,其中抛出了一个
InvalidOperationException异常。异常表示尝试禁用 2FA 但失败。应将这些事件视为异常并记录。这些事件可视为异常,应记录每一个事件。 -
为了修复事件日志记录的不足,我们重构了
OnGet方法,并在抛出InvalidOperationException异常时添加日志记录:public async Task<IActionResult> OnGet() { var customer = await _customerManager.GetUserAsync(User); if (customer == null) { return NotFound($"Unable to load customer with ID '{_customerManager.GetUserId(User)}'."); } if (!await _customerManager.GetTwoFactorEnabledAsync (user)) { _logger.LogError($"Cannot disable 2FA for customer with ID '{_customerManager .GetUserId(User)}' as it's not currently enabled."); throw new InvalidOperationException($"Cannot disable 2FA for customer with ID '{_customerManager.GetUserId(User)}' as it's not currently enabled."); } return Page(); } -
We also refactor the
OnPostAsyncmethod, as shown here:public async Task<IActionResult> OnPostAsync() { var customer = await _customerManager.GetUserAsync(User); if (customer == null) { return NotFound($"Unable to load customer with ID '{_customerManager.GetUserId(User)}'."); } var disable2faResult = await _customerManager.SetTwoFactorEnabledAsync (customer, false); if (!disable2faResult.Succeeded) { _logger.LogError($"Unexpected error occurred disabling 2FA for customer with ID '{_customerManager.GetUserId (User)}'."); throw new InvalidOperationException ($"Unexpected error occurred disabling 2FA for customer with ID'{_customerManager .GetUserId(User)}'."); } _logger.LogInformation("Customer with ID '{UserId}' has disabled 2fa.", _customerManager.GetUserId(User)); StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app"; return RedirectToPage("./TwoFactorAuthentication"); }我们使用依赖注入(DI中的
_logger对象调用LogError方法,该方法在当前日志提供程序中写入错误日志。
它是如何工作的…
我们已通过调用ConfigureLogging方法预先配置了我们的示例网上银行应用,以添加 Windows 事件日志记录。ConfigureLogging方法将为 WindowsEventLog提供程序创建一个ILogger对象。我们将ILogger对象的SourceName属性设置为OnlineBankingApp,以识别样本网上银行应用生成的日志:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureLogging(logging =>
{
logging.AddEventLog(eventLogSettings =>
{
eventLogSettings.SourceName = "OnlineBankingApp";
});
})
我们还通过在appsettings.json文件的Logging部分添加一个条目来配置信息日志记录。这将创建一个日志级别设置为Information的OnlineBankingApp类别:
{
"Logging": {
"EventLog": {
"LogLevel": {
"Default": "Warning",
"OnlineBankingApp": "Information"
}
},
有了这些设置,我们现在可以使用 WindowsEventLog提供程序进行日志记录。ILogger对象的实例_logger已经通过 DI 提供。我们现在只需在发生严重异常的代码行中调用ILogger对象的LogError方法。
笔记
需要注意的是,日志的位置和存储方式是基本标准。配置或代码不应将日志放置在与 web 服务器相同的位置。我们必须实施适当的访问控制,以防止未经授权查看日志。有一些开源和企业安全解决方案提供了安全查看、收集和存储日志的工具。
修复数据库事务记录不足的问题
基本数据库事务如创建、读取和删除记录对于审计跟踪至关重要,尤其是在执行数据库功能时发生错误时。
在此配方中,我们将修复在引发相关异常时失败的 DB 事务日志记录不足的问题。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
Open
Pages\Backups\Edit.cshtml.csand notice a lack of DB operation logging in theOnPostAsyncmethod:public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid){ return Page(); } _context.Attach(Backup).State = EntityState.Modified; try{ await _context.SaveChangesAsync(); } catch (DbUpdateException){ if (!BackupExists(Backup.ID)){ return NotFound(); } else{ throw; } } return RedirectToPage("./Index"); }但是,应记录数据库操作,如执行备份和更新其相关记录。
-
为了修复丢失的高值 DB 事务日志记录,让我们通过 DI 使用
ILogger接口添加一个记录器。首先定义类型为ILogger:public class EditModel : PageModel { private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context; private readonly ILogger<EditModel> _logger; // code removed for brevity的
_logger成员 -
接下来,将成员注入
EditModel构造函数中:public EditModel(OnlineBankingApp.Data .OnlineBankingAppContext context, ILogger<EditModel> logger) { _logger = logger; _context = context; } -
In the
try-catchblock, add the following lines of code:try{ await _context.SaveChangesAsync(); } catch (DbUpdateException ex){ if (!BackupExists(Backup.ID)){ _logger.LogError("Backup not found"); return NotFound(); } else{ _logger.LogError($"An error occurred in backing up the DB { ex.Message } "); throw; } }添加这些代码行将在
EventLog日志记录提供程序中使用前面配方中说明的相同日志记录设置写入错误日志。
它是如何工作的…
在敏感数据库操作(如数据库备份)期间,可能会发生意外错误。此类异常可能会在我们的示例网上银行应用中导致系统故障或更糟的攻击。我们通过跟踪这些数据库操作并了解事件发生的时间来降低丢失数据完整性的风险。我们调用LogError函数将日志写入 Windows 事件日志:
if (!BackupExists(Backup.ID)){
_logger.LogError("Backup not found");
return NotFound();
}
else{
_logger.LogError($"An error occurred in backing up the DB { ex.Message } ");
throw;
}
作为最佳实践,我们为我们预期的特定异常(提供适当的错误日志消息,最多只记录一般异常的Message属性,避免在我们创建的日志中披露敏感信息(更多关于中异常处理的最佳实践)第 13 章、最佳实践)。
修复过多的信息记录
正如我们在第 4 章敏感数据泄露中所了解到的,确保您防止个人信息泄露是确保应用安全的关键,日志信息也是如此。虽然日志很有用,但记录过多数据也有风险。犯罪者会找到获取有用信息的方法,而日志存储是他们试图发现的来源之一。
在此配方中,我们将修复过度记录的信息,如用户名和密码。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
打开
Areas\Identity\Pages\Account\Login.cshtml.cs并找到向日志发送过多敏感信息的代码行:if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var signInResult = await _signInManager.PasswordSignInAsync (Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure : false); if (signInResult.Succeeded) { _logger.LogInformation($"Customer with email { Input.Email } and password { Input.Password } logged in"); -
To fix the issue, we replace the line with a proper log entry:
if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var signInResult = await _signInManager.PasswordSignInAsync (Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); if (signInResult.Succeeded) { _logger.LogInformation("User logged in."); if (string.IsNullOrEmpty(HttpContext .Session.GetString(SessionKeyName))) { HttpContext.Session.SetString (SessionKeyName, Input.Email); }重构代码以删除用户名和密码等敏感信息,可防止犯罪者获得日志存储并使用收集到的信息利用我们的示例网上银行应用进行攻击。
它是如何工作的…
我们可以通过Run命令打开 Windows事件查看器来查看我们的示例网银应用生成的日志。以下是执行此操作的步骤:
-
Type Windows + R, and once the Run window shows up, as shown in the following screenshot, type
eventvwr.msc:![Figure 11.1 – Run command]()
图 11.1–运行命令
-
在事件查看器(本地)中,展开Windows 日志,然后展开应用。查找源名称相当于
OnlineBankingApp的日志:

图 11.2–事件查看器
您在事件查看器中看到的信息日志是前面配方中LogInformation方法调用生成的记录。我们修改了代码,以防止对敏感信息(如用户凭据)进行显式.t 日志记录。我们使用一个通用的信息消息来解决暴露细节的问题,我们不希望任何人滥用这些细节。
修复缺乏安全监控的问题
监控允许我们主动观察 ASP.NET Core web 应用中发生的事件。错过实时发生的事件会导致攻击者每分钟造成更大的伤害。开发人员必须在其 ASP.NET Core web 应用中启用监视,以便进行早期预防性检测。
在此配方中,我们将通过实现Azure Application Insights来修复我们示例网上银行 web 应用中缺乏安全监控的问题。
怎么做…
让我们来看看这个食谱的步骤:
-
在开始练习文件夹中,通过键入以下命令启动 VS 代码:
code . -
导航至菜单中的终端****新终端,或在 VS 代码中按Ctrl+Shift+即可。
** 通过在 VS 代码终端中运行以下命令来安装应用洞察软件开发工具包(SDK):dotnet add package Microsoft.ApplicationInsights.AspNetCore- Open
Startup.csand make a call to theAddApplicationInsightsTelemetrymethod underConfigureServices:
public void ConfigureServices(IserviceCollection services) { services.AddApplicationInsightsTelemetry(); // code removed for brevity顾名思义,
AddApplicationInsightsTelemetry方法将为我们的样本网上银行应用添加一个 Insights 遥测采集。- Open the
appsettings.jsonfile to add the instrumentation key:
{ "ApplicationInsights": { "InstrumentationKey": "My-Instrumentation-Key" }, "Logging": { "EventLog": { "LogLevel": { "Default": "Warning", "OnlineBankingApp": "Information" } },笔记
要生成插装密钥,请按照 Microsoft Azure Monitor 官方在线文档中的创建应用洞察资源说明进行操作,该文档位于https://docs.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource 。
- 在终端中键入以下命令以构建并运行示例应用:
dotnet run- Open a browser and go to
https://localhost:5001/:.
传入请求现在将由 Application Insights SDK 收集,包括具有
Medium、Error和Critical严重性的ILogger日志。* - Open
*## 它是如何工作的…
我们通过将示例网上银行 web 应用与Azure 应用洞察集成来实现遥测采集。Application Insights 收集的不仅仅是我们的ILogger提供商生成的性能指标和日志,它还执行分析和应用安全检测。此基于云的服务在出现安全问题时发送警报和通知,如不安全的表单、不安全的统一资源定位器(URL)访问和可疑的用户活动。
还有更多…
配方的前面步骤用于从服务器端启用遥测采集。以下是从客户端启用监视的步骤:
-
打开
Pages\_ViewImports.cshtml并从 Application Insights SDK 注入JavaScriptSnippet服务:@using OnlineBankingApp @namespace OnlineBankingApp.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet -
打开
Pages\Shared\_Layout.cshtml并在head部分插入 JavaScript 片段,同时调用Html.Raw助手方法:... <link rel="stylesheet" href="~/lib/bootstrap/ dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" /> @Html.Raw(JavaScriptSnippet.FullScript) </head>
通过在_Layout.cshtml文件中放置 JavaScript,我们可以在使用我们的示例网上银行 web 应用的布局模板的所有页面上启用客户端监控。*
十二、其他漏洞
OWASP Top 10实际上是安全专业人员用来了解最常见 web 应用漏洞的列表标准。它是开放式 Web 应用安全项目(OWASP组织的旗舰项目之一。正如您可能已经注意到的,本书第2-11章涵盖了 2017 年 OWASP 十大安全风险。根据 OWASP 中安全专家收集的信息,此报告每 3 到 4 年更改一次。新的或旧的风险可能会被引入或从此集合中删除,但本文档不是一个完整的列表,并且还有其他未涵盖的漏洞。本章将讨论更多的现有风险,其中一些风险已不再是 OWASP 前 10 名的一部分,但仍需了解。
在本章中,我们将介绍以下配方:
- 修复禁用的反跨站点请求伪造保护
- 防止服务器端请求伪造
- 防止原木注入
- 防止 HTTP 响应拆分
- 防止点击劫持
- 修正随机性不足
在本章中,您将学习如何使用 ASP.NET Core 的保护功能来防止跨站点请求伪造(CSRF)。您还将了解如何在您的代码中引入服务器端请求伪造(SSRF)以及防止其发生的方法。此外,您还将了解响应拆分是如何发生的,以及如何使用安全编码的基本原理之一,即验证,来增加保护。接下来,您将实施从 ASP.NET Core web 应用作为 HTTP 响应发送的frame-ancestor内容安全策略(CSP,以阻止点击劫持攻击。此外,您将练习一种基本的消毒技术,以阻止攻击者注入和伪造您的日志。最后,您将学习如何生成加密安全的随机数,以保护哈希密码不被解密。
技术要求
本书是为使用 Visual Studio 代码 Git、.NET 5.0 编写和设计的。ASP.NET Core Razor 页面中提供了配方中的代码示例。示例解决方案还将使用 SQLite 作为数据库引擎,以简化设置。本章完整的代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter12 。
修复禁用的防跨站点请求伪造保护
浏览器和 web 服务器之间有一种固有的信任,对手经常会滥用这种信任。web 应用的用户(通常由 ASP.NET Core web 应用发出经过身份验证的会话)被攻击者诱骗,通过访问恶意网站或与恶意网站交互来执行无意行为。这种攻击方法通过使浏览器从恶意网站发送巧尽心思构建的请求,滥用用户已建立的身份验证状态。此跨站点请求伪造(CSRF)漏洞提示我们检查代码并启用请求验证,我们将在本配方中了解这一点。
准备好了吗
对于本章中的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆 ASP.NET Secure Codeing Cookbook 存储库下载示例网上银行应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter12\cross-site-request-forgery\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
在此配方中,我们将修复禁用 CSRF 保护的剃须刀页面。
怎么做…
让我们来看看这个食谱的步骤:
-
在启动练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Startup.csand locate the code inConfigureServicesthat configures the filter to skip the anti-forgery token validation process:services.AddRazorPages(options => { options.Conventions.AuthorizeAreaFolder("Identity, "/Account/Manage"); options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout"); }) .AddRazorPagesOptions(options => { options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()); });将
IgnoreAntiforgeryTokenAttribute的实例传递给ConfigureFilter方法会将该属性全局添加到我们的示例网上银行 web 应用中。 -
Change the type of token attribute that you pass into the
ConfigureFiltermethod and replace it withAutoValidateAntiforgeryTokenAttribute:.AddRazorPagesOptions(options => { options.Conventions.ConfigureFilter(new AutoValidateAntiforgeryTokenAttribute()); });AutoValidateAntiforgeryTokenAttribute将要求使用从 HTTP POST 等不安全 HTTP 谓词接收的防伪令牌进行请求验证。 -
Next, open
Pages\Loans\Create.cshtml.csand find theIgnoreAntiforgeryTokenattribute, which annotates theOnlineBankingApp.Pages.Loans.CreateModelview model:using OnlineBankingApp.Models; using System.Security.Claims; namespace OnlineBankingApp.Pages.Loans { [IgnoreAntiforgeryToken(Order = 1001)] public class CreateModel : PageModel { private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context; // code removed for brevity虽然我们已经在
Startup.cs中全局重新启用了请求验证,但我们仍然可以使用IgnoreAntiforgeryToken在每个视图模型上禁用 CSRF 保护。 -
Replace this attribute from
PageModelwithAutoValidateAntiforgeryTokento explicitly enable anti-forgery tokens to be sent to unsafe HTTP requests:using OnlineBankingApp.Models; using System.Security.Claims; namespace OnlineBankingApp.Pages.Loans { [AutoValidateAntiforgeryToken] public class CreateModel : PageModel { private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context; // code removed for brevity笔记
因为我们已经从
Startup.cs全局启用了AutoValidateAntiforgeryToken,所以很高兴知道我们可以在操作级别使用此属性。
它是如何工作的…
在处理跨站点请求时,常见的错误是禁用防伪令牌的验证,以使此请求能够跨不同的域进行。在我们的示例网上银行 web 应用中,我们在配置 Razor 视图的AddRazorPagesOptions中全局禁用了此功能,并将过滤器设置为IgnoreAntiforgeryTokenAttribute,从而从每个页面中删除请求验证:
.AddRazorPagesOptions(options =>
{
options.Conventions .ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
});
类似地,您可以在类级别通过使用IgnoreAntiforgeryToken注释类来禁用此功能。与此属性一起指定一个编号为1001的Order,以确定过滤器的执行顺序:
[IgnoreAntiforgeryToken(Order = 1001)]
每种方法都会带来巨大的风险,因此必须生成防伪令牌,并由 web 服务器验证请求,以防止 CSRF 并验证其真实性。因此,我们使用AutoValidateAntiforgeryTokenAttribute来配置过滤器:
.AddRazorPagesOptions(options =>
{
options.Conventions .ConfigureFilter(new AutoValidateAntiforgeryTokenAttribute());
});
另一种方法是在类级别,您可以在类定义的顶部用AutoValidateAntiforgeryToken注释类:
[AutoValidateAntiforgeryToken]
public class CreateModel : PageModel
前面的代码支持对不安全 HTTP 方法的防伪令牌进行验证,这些方法是除GET、OPTIONS、HEAD和TRACE之外的 HTTP 谓词。
还有更多…
同步器令牌模式(STP是一种安全编码和开发模式,其中令牌作为请求的一部分发送到 web 服务器,以证明其真实性。然后,web 服务器验证令牌以确认其真实性。可以使用全局唯一标识符(GUID)手动创建令牌,但建议依赖.NET Framework 生成的内置令牌。
反伪造令牌也以隐藏字段的形式从 HTML 元素生成。单独生成这些令牌并不是阻止代码中 CSRF 漏洞的完整解决方案。还必须启用服务器端的请求验证。
基于 JavaScript 的应用可以进行 AJAX 调用,以使用这些防伪令牌发送 HTTP 请求。客户端脚本将显式查找__RequestVerificationToken隐藏的表单字段并检索其值。下面是一个例子,展示了在 HTML 页面中呈现时__RequestVerificationToken的样子:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NZYeppsy75Pga3ZTfYs_GOXLIV7qV8nWInxNkWX2KTNH5O6U08mXz8qZYB3UPum5QFFO0zm E1IyE8her6r0wf85eobep6SYfJCP6UBDeTe9Jpao8cEgdiUYK yY5IWfQX4MhzYupGn5uciC74mbfQ0U" />
如果请求验证失败,则可能是表单标记帮助器的asp-antiforgery值设置为false。始终注意此标记,将其设置为true或删除asp-antiforgery属性,使__RequestVerificationToken隐藏表单字段默认自动呈现:
<form method="post" asp-antiforgery="true" >
在 Razor 页面中生成令牌的另一种方法是使用AntiForgeryTokenHTML 助手方法。将以下代码放入标记中:
@Html.AntiForgeryToken()
AntiForgeryTokenHTML 帮助程序将使用加密值创建__RequestVerificationToken隐藏表单字段。
防止服务器端请求伪造
ASP.NET Coreweb 应用由不同的层和组件组成,使其成为一个完整的工作系统。大多数情况下,它需要一个后端服务来处理或向基础 web 应用提供数据。这些不同的服务相互连接,形成一个内聚的、功能强大的 web 应用。这可以通过 web 服务或系统内部或外部托管的基于 REST 的 API 的形式完成,然后我们的代码调用这些 API 和 web 服务(或微服务)的操作。
但是,如果没有适当的筛选或无法验证发送到这些服务的数据,主机可能会开始执行意外操作。此漏洞也称为服务器端请求伪造服务器端请求伪造(SSRF),对手利用缺乏有效验证或清理的漏洞进行攻击。
准备好了吗
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter12\server-side-request-forgery \before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
我们还需要安装旧版本的 MongoDB(3.4 版)。按照上的安装 MongoDB说明进行操作 https://docs.mongodb.com/v3.4/installation 。
安装后,确保mongod服务可执行文件与--rest开关一起运行。这将启用 MongoDB 的 RESTAPI。请参见以下示例:
C:\Program Files\MongoDB\Server\3.4\bin\mongod.exe" --rest --service --config="C:\Program Files\MongoDB\Server\3.4\mongod.cfg"
通过运行以下命令,使用数据填充 MongoDB:
db.createCollection('Payees')
另外,运行以下命令:
db.Payees.insertMany([{'Name':'Mint Mobile','Address':'P.O. Box 15124 Albany, NY 12212-5124','PhoneNo':'1(800) 683-7392','AccountNo':'8244-1044','Description':'Business Line'},{'Name':'Private Internet Access VPN','Address':'5555 DTC Parkway, Suite 360\. Greenwood Village, CO 80111','PhoneNo':'(720) 277-9121','AccountNo':'6510-2236','Description':'VPN Personal Subscription'}])
第一个 MongoDB 命令db.createCollection创建了我们的Payees集合,我们将在示例网上银行应用中使用它。db.Payees.insertMany将数据添加到我们的Payees集合中。
现在,我们准备完成这个食谱。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
\Pages\Payees\Index.cshtml.csand go to theOnGetAsyncmethod, where a call toPayeeServiceis executed:public async Task OnGetAsync() { var mongouri = Request.Query["mongouri"]; mongouri = string.IsNullOrWhiteSpace(mongouri) ? "http://localhost:28017/test/Payees/" : Request.Query["mongouri"]; Roots = await _payeeService.GetPayeesAsync (mongouri); }注意,代码接受来自
mongouri查询字符串参数的输入。mongouri是 MongoDB REST 接口的 URL,在该接口中检索收款人列表。攻击者可能会利用此漏洞,并使用其可用的 REST API 操作将任意 URL 或滥用发送到 MongoDB 实例。这不需要访问控制或检查。 -
为了修复易受攻击的代码,我们可以使用正则表达式添加验证,以确保 URL 的格式符合我们的预期。我们将首先添加对
Regex类名称空间的引用;也就是说,System.Text.RegularExpression:using System.Text.RegularExpressions; -
接下来,创建一个新方法,并使用以下代码将其命名为
IsValidMongoRestUri:private bool IsValidMongoRestUri(string mongouri) { string pattern = @"^http://localhost:28017/ test/Payees/\\?$"; Regex regex = new Regex(pattern, RegexOptions .IgnoreCase); return regex.IsMatch(mongouri); } -
添加对
System.Http.Net命名空间的引用。 -
Invoke the
IsValidMongoRestUrimethod to validate themongouristring that was passed via the query string:public async Task OnGetAsync() { var mongouri = Request.Query["mongouri"]; if (string.IsNullOrWhiteSpace(mongouri)) { mongouri = "http://localhost:28017/test/Payees/"; } else { if(!IsValidMongoRestUri(mongouri)) { throw new HttpRequestException("Invalid Request"); } } Roots = await _payeeService.GetPayeesAsync (mongouri); }前面的代码在
mongouri为意外格式时也会抛出一个HttpRequestException。
它是如何工作的…
当我们盲目地允许控制一个querystring参数时,这种攻击向量为坏行为人提供了利用我们的示例网上银行 web 应用的途径。我们使用mongouri查询字符串检索 REST API 端点。由于缺少验证,恶意用户可以传递可能执行不需要的操作的 URI。例如,我们可以将此值赋给mongouri参数,MongoDB REST API 将执行此操作:
mongouri=http://localhost:28017/admin/$cmd/?
filter_eval=function()
{ifdb.version().charAt(0)=='3'){sleep(2000)}}&limit=1';
分解mongouri的值,它包含一系列连续的 MongoDB 语句和函数,如果 MongoDB 版本的第一个字符(在本例中为3)等于 true,将导致 JavaScript 执行上下文暂停 2 秒。如果我们要以一种漂亮的格式打印它,下面是这些连续语句和函数的样子:
filter_eval=function(){
if(db.version().charAt(0)=='3'){
sleep(2000)
}
}
为了防止不必要地执行任意命令,我们可以使用正则表达式验证 URL 是否符合我们预期的格式,从而阻止任何人通过其 HTTP 接口使用 MongoDB 的db命令。我们创建了一个名为IsValidMongoRestUri的布尔方法,用于检查它接收到的mongouri值是否与我们期望的 URL 匹配:
private bool IsValidMongoRestUri(string mongouri)
{
string pattern = @"^http://localhost:28017/test/
Payees/\\?$";
Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
return regex.IsMatch(mongouri);
}
在这种情况下,使用安全编码的基础之一是正确的输入验证,以防止 SSRF。
还有更多…
除了 URI 验证之外,还可以使用Uri.CheckHostName或IPAddress.TryParse方法。Uri.CheckHostName帮助我们检查 DNS 或域是否有效,IPAddress.TryParse验证 IP 地址是否有效。
防止日志注入
在第 11 章**记录和监控不足中,我们了解了记录的重要性。日志记录为我们提供了必要的可见性,以了解 ASP.NET Core web 应用中的一系列重要事件。但是,如果我们创建的用户控制的日志信息未经验证,黑客也可以利用日志记录进行攻击。如果日志条目中存在恶意输入,还可以利用日志查看器的漏洞(如果存在)。
*例如,基于 web 的日志查看器可能存在跨站点脚本漏洞,使用 XSS 负载以及数据查看日志条目可以利用此漏洞。在此配方中,我们将通过实现输入清理来防止代码中的日志注入漏洞。
准备好了吗
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter12\log-injection\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Pages\Loans\Create.cshtml.csand locate the code inOnPostAsync, where it logs a warning when the app is unable to submit a loan:if (await TryUpdateModelAsync<Loan>( emptyLoan, "loan", l => l.ID, l => l.CustomerID, l => l.Amount, l => l.PeriodInMonths, l => l.TransactionDate, l => l.Note)) { _context.Loan.Add(emptyLoan); await _context.SaveChangesAsync(); } else { _logger.LogWarning("Problem creating loan:" + emptyLoan.CustomerID + ";" + emptyLoan.Amount + ";" + emptyLoan.PeriodInMonths + ";"+ emptyLoan.Note); }在不清理输入的情况下创建日志条目可能会使我们的示例网上银行 web 应用暴露于日志注入。
-
Applying what we have learned from the basics of secure coding from Chapter 1, Secure Coding Fundamentals, we can use methods such as
String.ReplaceorRegex.Replaceto replace malicious characters from the input. We will begin by validating whetherNoteis not empty or null before we sanitize the input:else { if (!String.IsNullOrEmpty(emptyLoan.Note)) { emptyLoan.Note = Regex.Replace(value, @"^[a zA-Z0-9 ]+$", string.Empty); } _logger.LogWarning("Problem creating loan:" + emptyLoan.CustomerID + ";" + emptyLoan.Amount + ";" + emptyLoan.PeriodInMonths + ";" + emptyLoan.Note); }通过使用
Regex.Replace方法,我们可以使用模式匹配来替换日志中潜在的危险字符。
它是如何工作的…
在我们的示例网上银行 web 应用中,我们有一个贷款申请页面,可以在其中提交贷款申请。在此页面上,我们可以输入诸如贷款金额、贷款期限和可选注释等信息,这些信息以自由格式文本形式提供。
如果在保存贷款提交时出现问题,我们可以记录输入的信息,包括未验证的Note数据:
_logger.LogWarning("Problem creating loan:" + emptyLoan .CustomerID + ";"
+ emptyLoan.Amount + ";"
+ emptyLoan.PeriodInMonths + ";"
+ emptyLoan.Note);
虽然可以为该银行事务创建一个日志条目,以了解其未能保存的原因,但该注释可以作为日志注入的入口点。我们可以通过使用Replace.Regex方法对Note字段进行消毒来防止日志注入:
if (!String.IsNullOrEmpty(emptyLoan.Note))
{
emptyLoan.Note = Regex.Replace(value, @"^[a-zA-Z0-9 ]+$", string.Empty);
}
我们可以在调用Regex.Replace方法之前验证Note是否为 null 或空。我们可以在这里使用[1]+$正则表达式模式,它指定字母数字字符和空格仅允许作为Note的有效输入。
还有更多…
如果日志条目的某些部分由用户控制且未经验证或消毒,则攻击者可以通过向日志提供伪造和虚假信息来利用此漏洞。对手可以向我们的日志中注入非敏感信息,并留下大量噪音,从而阻止有效的安全事件分析。此漏洞称为原木伪造。此配方中提供的代码也应该能够缓解此安全漏洞。
防止 HTTP 响应拆分
HTTP 响应拆分或CRLF 注入是另一个注入漏洞,攻击者可以发送未过滤的 HTTP 请求,其中包括回车符和换行符。在请求中允许回车符(%0d,URL 编码形式)和换行符(%0a,同样 URL 编码形式)会在 HTTP 响应头中引入拆分,从而改变 ASP.NET Core web 应用的行为。此 HTTP 响应头修改可能导致利用许多其他漏洞,例如开放重定向或跨站点脚本等。
在此配方中,我们将通过向\n和\r字符添加验证检查来防止代码中的 HTTP 响应分裂。
准备好了吗
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter12\crlf-injection\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
打开
Areas\Identity\Pages\Account\Login.cshtml.cs并找到代码行,在这些代码行中,参数可能会被注入回车符和换行符。 -
注意
OnGetAsync方法public async Task OnGetAsync(string returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { ModelState.AddModelError(string.Empty, ErrorMessage); } returnUrl ??= Url.Content("~/"); //code removed for brevity中突出显示的代码
-
Also, notice the highlighted code in the
OnPostAsyncmethod:public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthentication SchemesAsync()).ToList(); //code removed for brevity在这两种方法中,
returnUrl是我们需要验证的论点。 -
为了弥补潜在的安全漏洞,我们将添加一个名为
SplitExist的新方法。helper 是一个名为SplitExist的布尔方法,它将字符串作为输入,并验证每个字符,以检查是否有任何 CRLF 注入尝试:private bool SplitExist(string input) { return input.FirstOrDefault(c => c == 0x13 || c == 0x10) != 0 ? true : false; } -
使用
SplitExist方法验证returnUrl。重构OnGetAsync中的代码,如下所示:public async Task OnGetAsync(string returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { ModelState.AddModelError(string.Empty, ErrorMessage); } if (!string.IsNullOrEmpty(returnUrl)) { if(SplitExist(returnUrl)) { throw new InvalidOperationException(string .Format("Invalid character in the return URL")); } } else { returnUrl = Url.Content("~/"); } -
Also, refactor the code in
OnPostAsync, as follows:public async Task<IActionResult> OnPostAsync(string returnUrl = null) { if (!string.IsNullOrEmpty(returnUrl)) { if(SplitExist(returnUrl)) { throw new InvalidOperationException (string.Format("Invalid character in the return url")); } } else { returnUrl = Url.Content("~/"); } ExternalLogins = (await _signInManager .GetExternalAuthenticationSchemes Async()).ToList();如果
returnUrl参数不为空,则调用SplitExist方法检查字符串是否包含回车符或换行符字符。
它是如何工作的…
如果存在 HTTP 响应拆分漏洞,这是由于缺少或不正确地验证可能的入口点和输入造成的。在此配方中,我们指出returnUrl是一个潜在目标,因此我们添加了一个名为SplitExist的新方法,用于检查字符串中的回车符和换行符:
private bool SplitExist(string input)
{
return input.FirstOrDefault(c => c == 0x13 || c == 0x10) != 0
? true
: false;
}
此方法逐个字符检查returnUrl字符串,并验证其任何字符既不是0x13(相当于回车符的 ASCII 码),也不是0x10(相当于换行符的 ASCII 码)。
笔记
此漏洞以前存在于 ASP.NET Core 早期版本(ASP.NET Core 版本 5,候选版本 1)的框架内,但在后续版本中已解决。
还有更多…
考虑到 ASP.NET Core 团队已经实施了他们自己的角色验证,可能不再需要实施此配方中的验证方法。
例如,当示例网上银行应用处于运行状态时,尝试打开浏览器并转到https://localhost:5001/Identity/Account/Login?ReturnUrl=%2FAccount%0D%0ALocation%3A%20http%3A%2F%2Fwww.packtpub.com 。
请注意页面上显示的异常:

图 12.1–无效操作异常
web 应用抛出了一个InvalidOperationException,因为 ASP.NET 框架检测到 URL 中存在回车符和换行符。如果请求成功,它将拆分 HTTP 请求,添加一个locationHTTP 头并将用户重定向到www.packtpub.com:
HTTP/2 302 Found
cache-control: no-cache, no-store
date: Tue, 11 May 2021 17:18:39 GMT
pragma: no-cache
content-type: text/html; charset=utf-8
location: http://www.packtpub.com
server: Kestrel
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
尽管有检查,ASP.NET Core 框架是一个不断增长和变化的平台。将来可能在代码中引入或不引入漏洞。
防止点击劫持
点击劫持发生时,您的 web 应用允许自己在恶意网站中呈现(通常通过 iFrame),从而改变整个 UI。然后,用户将看到一个外观不同的页面,通过让用户认为他们正在与欺骗页面交互,而实际上他们正在输入信息并单击 web 应用上的按钮,诱使他们在 web 应用上执行未知操作。
为了防止 ASP.NET Core web 应用成为点击劫持攻击的受害者,您可以实施 CSP,以阻止恶意网站的 IFrame 呈现您的 web 应用。
准备好了吗
我们将使用上一个配方中使用的网上银行应用。运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter12\clickjacking\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
概念验证(PoC)
要了解点击劫持的威胁,请遵循并遵守以下步骤:
-
打开
wwwroot\iframe-demo.html并注意,我们的示例网上银行 web 应用已放置在 IFrame:<!DOCTYPE html> <html> <body> <h1>iframe Demo</h1> <iframe src="https://localhost:5001/Identity/Account /Login?ReturnUrl=%2F" title="iFrame Demo"></iframe> </body> </html>中
-
导航至菜单中的终端****新终端,或在 Visual Studio 代码中按Ctrl+Shift+即可。
** Type the following command in the Terminal to build and run the sample app:dotnet run此步骤对于我们在 IFrame 中查看示例网上银行 web 应用是必要的。
- 在浏览器中打开
wwwroot\iframe-demo.html,观察我们的示例网上银行 web 应用在 IFrame 中呈现:*
- 在浏览器中打开
*
图 12.2–IFrame 内的网上银行 web 应用
想象一个对手使其网页比我们的概念验证(PoC更具欺骗性。这可能导致您在我们的示例网上银行 web 应用中单击或执行不需要的交易。
笔记
clickjacking 漏洞是通过错误地抑制X-Frame-Options: sameoriginHTTP 响应头(默认情况下随每个响应一起发送)而实现的:
services.AddAntiforgery(options =>{
options.SuppressXFrameOptionsHeader = true;
});
怎么做…
使用 Visual Studio 代码,打开\Chapter12\click-jacking\before\OnlineBankingApp\处的样本网银应用文件夹。
在此文件夹中执行以下步骤以添加内容安全策略,以防止 web 应用在 IFrame 中呈现:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Startup.csand look at theUsemethod call inConfigure. The middleware is adding the necessary security headers, as we learned in Chapter 7, Security Misconfiguration:app.Use(async (context, next) => { context.Response.Headers.Add("X-XSS- Protection", "1; mode=block"); context.Response.Headers.Add ("X-Content-Type-Options", "nosniff"); await next(); });请注意,没有 HTTP 响应头阻止 web 应用在 IFrame 中呈现。
-
To include a content security policy HTTP header, we can add a new line:
app.Use(async (context, next) => { context.Response.Headers.Add("X-XSS- Protection", "1; mode=block"); context.Response.Headers.Add("X-Content-Type- Options", "nosniff"); context.Response.Headers.Add("X-Frame-Options", "DENY"); context.Response.Headers.Add("Content-Security- Policy", " frame-ancestors 'none'"); await next(); });这些 HTTP 响应头用于安全目的,将在下一节中解释。
-
删除以下抑制
X-Frame-Options: sameorigin标题的代码行:services.AddAntiforgery(options => { options.SuppressXFrameOptionsHeader = true; }); -
Alternatively, you can assign
SuppressXFrameOptionHeaderwith a value offalse:services.AddAntiforgery(options => { options.SuppressXFrameOptionsHeader = false; });这将取消抑制
X-Frame-Options报头,并将其作为 HTTP 响应的一部分提供。
验证 CSP HTTP 头
遵循以下步骤:
-
导航到菜单中的终端****新终端或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/。* Log in using the following credentials:
a) 。电邮:
stanley.s.jobson@lobortis.cab) 。密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the Online Banking app's Home page:
![Figure 12.3 – Home page authenticated]()
图 12.3–主页验证
- 按F12打开浏览器的开发者工具。* 进入网络选项卡,在流量列表中选择第一条 HTTP 流量。* Once you've selected a form of HTTP traffic, scroll through the right-hand pane to view the corresponding HTTP response security headers:
![Figure 12.4 – CSP HTTP response header]()
图 12.4–CSP HTTP 响应头
内容安全策略 HTTP 安全标头现在已添加为从我们的示例网上银行 web 应用发送的 HTTP 响应的一部分,从而保护其免受点击劫持攻击。
- 重复本配方点击劫持概念验证(PoC)部分中提供的步骤,注意我们的示例网上银行 web 应用不再在 IFrame 中呈现:*
- 打开浏览器并转到
*
图 12.5–框架被拒绝
为frame-ancestors添加 CSP 并重新启用X-Frame-OptionsHTTP 头将阻止我们的在线银行 web 应用示例被框接。
它是如何工作的…
安全性标题,如内容安全策略和X-Frame-Options有助于保护我们的 ASP.NET Core web 应用免受各种漏洞的攻击,包括点击劫持。如果启用或禁用此类服务,此漏洞会传播。
还有一些有效的理由来考虑为什么你想让你的 Web 应用呈现在 iFrAME 中。与内容管理系统的某些集成需要框架,因此解决这一问题的最佳方法是在 CSP 中指定白名单主机:
Content-Security-Policy: frame-ancestors 'self' https://www.packpub.com
self值表示我们允许我们的示例网上银行 web 应用在 IFrame 中呈现,该 IFrame 来自我们自己的来源(不包括子域)。https://www.packpub.com的值指定我们允许 Packt 网站将我们的 web 应用放置在其 IFrame 中。
修正随机性不足
伪随机数字可能足以用于不太关键的操作,但这些数字并不是真正的随机数字。计算机使用数学公式来产生这些伪随机数,但它们的随机性不足以用于加密操作,如创建 salt。这些随机方法和函数生成器生成的数据的可预测性和确定性增加了密码哈希被破解的可能性,从而导致哈希冲突攻击。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter13\insufficient-randomness\before\OnlineBankingApp的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在终端中键入以下命令来构建示例应用,以确认没有编译错误:
dotnet build -
打开
\Chapter13\insufficient-randomness\before\OnlineBankingApp\Areas\Identity\PasswordHasher.cs文件,注意CreateSalt方法:private byte[] CreateSalt() { var buffer = new byte[SaltBytes]; Random rnd = new Random(); rnd.NextBytes(buffer); return buffer; } -
CreateSalt方法使用Random类生成 salt,并调用NextBytes填充缓冲区,缓冲区是一个字节数组。虽然NextBytes方法在生成数字和字节的随机序列方面做得不错,但对于我们的 salt 生成来说,它还不够强大。 -
We can refactor our code to use
RNGCryptoServiceProviderto generate a cryptographically secure and random number instead, replacing theRandomclass we used previously:private byte[] CreateSalt() { var buffer = new byte[SaltBytes]; var rng = new RNGCryptoServiceProvider(); rng.GetBytes(buffer); rng.Dispose(); return buffer; }GetBytes方法加载一个加密强的随机字节数组。
它是如何工作的…
salt 是一段随机数据,包含在敏感信息(如密码)中,然后将其输入哈希方法或函数并保存在持久性存储中。Salt 提供了额外的保护,因为这段任意数据会降低密钥或密码的可预测性。
我们可以使用类RNGCryptoServiceProvider生成随机数,以保护散列密码不被破译:
var rng = new RNGCryptoServiceProvider();
rng.GetBytes(buffer);
笔记
与伪随机数生成器(PRNG****类,如Random相比,使用RNGCryptoServiceProvider生成字节随机数组时存在性能差异,但RNGCryptoServiceProvider提供了更多的随机性质量,并且在加密方面更好。
**下面是代码的其余部分,它调用了CreateSalt方法。有一个GetHash方法期望散列作为第二个参数,以及要散列的密码。如果 salt 为null,则会调用CreateSalt方法生成 salt:
private string GetHash(string password, byte[] salt)
{
var saltBytes = salt ?? CreateSalt();
var argon2 = new Argon2id(Encoding.UTF8 .GetBytes(password))
{
Salt = saltBytes,
DegreeOfParallelism = Threads,
Iterations = Iterations,
MemorySize = Memory
};
saltBytes的值是用来配置Argon2id类构造函数的Salt属性来散列密码。*****
十三、最佳实践
总的来说,ASP.NET Core web 应用的安全性通常取决于开发人员实施安全措施和编写安全代码所采取的步骤。在本书前面的章节和菜谱中,我们已经了解了不安全的代码是什么样子,这些弱点带来的风险,以及最重要的是,如何缓解这些安全问题。但是,除了安全编码的基础知识之外,还有一些行之有效的安全编码方法。这是因为它们支持.NET 框架中可用的必要防御或保护机制,我们将在本章中对所有这些机制进行研究。
在最后一章中,我们将介绍以下配方:
- 正确的异常处理
- 使用与安全相关的 cookie 属性
- 使用内容安全策略
- 修复剩余的调试代码
在本章结束时,您将学习如何安全地处理错误和异常,在 cookie 中使用有助于保护您的应用免受各种安全威胁的属性,应用内容安全策略(CSP)在 ASP.NET Core web 应用中使用的资源中创建信任边界,最后学习如何正确编写调试代码。
技术要求
本书是为配合 VisualStudio 代码、Git 和.NET5.0 而编写和设计的。这些配方中的代码示例将在 ASP.NET Core Razor 页面中提供。示例解决方案还使用 SQLite 作为数据库引擎,以简化设置。本章的完整代码示例可在上找到 https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter13 。
准备好了吗
对于本章中的食谱,我们需要一个在线银行应用示例。
打开命令 shell 并通过克隆 ASP.NET Secure Codeing Cookbook 存储库下载示例网上银行应用,如下所示:
git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git
运行示例应用以验证没有生成或编译错误。在命令 shell 中,导航到位于\Chapter13\exception-handling\before\OnlineBankingApp的示例应用文件夹,并运行以下命令:
dotnet build
dotnet build命令将构建示例OnlineBankingApp项目及其依赖项。
正确的异常处理
处理错误和异常的实践是干净高效编码的一部分。这项技术是在开发中添加的,以使我们的代码更具可读性和可维护性。但通常情况下,由于错误处理不当,代码中会出现错误。这句话不仅适用于普通的 bug。但也有安全漏洞。错误处理异常是因为捕捉这些异常的方法不正确,从而导致不必要的利用。
在此配方中,我们将修复异常的不当处理,并防止我们的示例网上银行 web 应用吞咽异常。
准备好了吗
使用 Visual Studio 代码,打开位于\Chapter13\exception-handling\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Services\KnowledgebaseService.csand observe thetry-catchblock in theSearchmethod:using (XmlReader reader = XmlReader.Create(file, settings)) { try { XDocument xmlDoc = XDocument.Load(reader); // code removed for brevity return searchResult; } catch (Exception){ return searchResult; } }请注意,
try-catch块只捕获一个通用异常。catch 块还通过吞咽异常来忽略我们的示例网上银行 web 应用中可能发生的异常。作为最佳实践,我们需要对异常处理进行具体说明。因此,让我们通过指定一种可能发生的类型
Exception来重构代码,该类型最接近代码块试图执行的内容。 -
To apply the best practice in exception handling, we must add new catch blocks and rethrow the exception by instantiating its corresponding
FileFormatExceptionandXmlExceptiontypes:using (XmlReader reader = XmlReader.Create(file, settings)) { try { XDocument xmlDoc = XDocument.Load(reader); // code removed for brevity return searchResult; } catch (XmlException ex){ _logger.LogCritical(String.Format("Reader error: {0}", ex.Message)); throw new XmlException(ex.Message); } catch (Exception ex){ _logger.LogCritical(String.Format("Reader error: {0}", ex.Message)); throw new XmlException(ex.Message); } }笔记
抛出一个新的
Exception或XmlException实例将使我们丢失异常的原始细节,其中包括堆栈跟踪信息。这种方法的缺点是,如果在生产过程中出现问题,那么调试应用将更加困难。但是,从堆栈跟踪泄漏敏感信息的风险将降至最低。
它是如何工作的…
忽略异常而不捕获它们是不好的做法,也是 ASP.NET Core web 开发人员应该避免的习惯。忽略错误条件是坏行为人利用并导致恶意行为被发现或标记的途径。确保以较少的细节重新显示异常,以防止信息泄漏。我们可以通过捕获特定的异常并收集异常的Message属性来实现这一点,这样就可以将其传递到我们使用 rethrow 创建的新实例中:
catch (XmlException ex) {
throw new XmlException(ex.Message);
}
由于我们正在处理 XML 文件,我们应该预料到可能会出现XmlException类型。我们可以为这个特定的异常类型添加一个新的 catch 语句。如果我们有处理每种类型异常的特定方法,那么对泛型Exception类型使用单个 catch 是没有用的。
还有更多…
未处理的异常也由 web 服务器处理。对于我们的示例网上银行 web 应用,Kestrel web 服务器处理应用引发的异常,并在发送其余 HTTP 头之前发送 HTTP500 Internal Error状态代码响应。红隼随后关闭连接。如果跟踪作为响应的一部分发送,则此事件可能导致暴露异常详细信息中的堆栈跟踪。遵循此配方中的步骤将防止泄漏异常详细信息。
使用安全相关 cookie 属性
Cookie 是 web 应用开发的重要组成部分。它是一种在无状态 HTTP 协议中维护状态的方法,并携带在安全性中使用的最重要的信息,如令牌和会话数据。正如我们在第 7 章、安全配置错误的通过不安全 cookie修复信息暴露的配方中所了解的,我们启用或禁用 cookie 的保护以防滥用的 cookie 属性。在这个配方中,我们了解了安全和仅 HTTP 属性如何限制我们的 cookie,因为它们要么只能通过安全传输发送,要么保存在浏览器中,要么防止任意客户端脚本读取它们的敏感值。
在这个配方中,我们将学习如何使用另一个与安全相关的 cookie 属性SameSite。SameSite 是一个相对较新的 cookie 属性(在撰写本文时),用于限制第三方网站访问标有第一方上下文的 cookie。
准备好了吗
我们将使用上一个配方中使用的在线银行应用。使用 Visual Studio 代码,打开位于\Chapter13\secure-cookie-policy\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
打开
Startup.cs并开始向中间件全局添加 cookie 策略,方法是添加对Microsoft.AspNetCore.CookiePolicy命名空间的引用:using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.CookiePolicy; -
Next, we must add the following lines of code under the
ConfigureServicesmethod:public void ConfigureServices(IserviceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.MinimumSameSitePolicy = SameSiteMode.Strict; options.Secure = Environment.IsDevelopment() ? CookieSecurePolicy.None : CookieSecurePolicy.Always; options.HttpOnly = HttpOnlyPolicy.Always; }); // truncated前面的示例为我们的样本网上银行 web app 生成的 cookie 添加了必要的安全相关 cookie 属性,并添加了
MinimumSameSitePolicy,用于控制我们的样本网上银行 web app cookie 的SameSite属性的行为。cookie 策略选项的MinimumSameSitePolicy属性被分配了SameSiteMode.Strict值,将SameSitecookie 属性标记为Strict值。 -
要使用存储在 cookie 中的反 CSRF 令牌实现相同的安全 cookie 策略,请将
SecurePolicy属性分配给CookieSecurePolicy.Always枚举值:services.AddAntiforgery(options => { options.SuppressXFrameOptionsHeader = false; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); -
虽然通过
ConfigureServices设置的全局 cookie 策略将使 cookie 中的反 CSRF 令牌标记为SameSite属性,但您也可以将此属性显式指示给SameSiteMode.Strict。这意味着,如果反 CSRF cookie 位于同一站点请求中,则仅将其发送到服务器:services.AddAntiforgery(options => { options.SuppressXFrameOptionsHeader = false; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; }); -
如果关注来自会话中间件的会话状态 cookie,您可以通过将相同的
SameSite属性分配给SameSiteMode.Strict:services.AddSession(options => { options.Cookie.Name = ".OnlineBanking.Session"; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.IdleTimeout = TimeSpan.FromSeconds(10); });来将会话状态 cookie 限制在第一方上下文中
-
Execute the cookie policy middleware by making a call to the
UseCookiePolicymethod inConfigure:app.UseRouting(); app.UseCookiePolicy(); app.UseAuthentication();UseCookiePolicy在被调用时,应用我们在ConfigureServices中定义的全局 cookie 策略。
它是如何工作的…
将SameSitecookie 属性设置为Strict将限制 cookie 何时作为请求的一部分发送的上下文。顾名思义,cookie 仅在同一站点和第一方请求中严格发送,从而防止第三方网站或 web 应用在启动请求时发送 cookie。这可以通过会话和反伪造服务选项进行配置:
options.Cookie.SameSite = SameSiteMode.Strict;
这也可以在全局 cookie 策略选项中完成:
options.MinimumSameSitePolicy = SameSiteMode.Strict;
关于 SameSite 属性的一些细微差别,有几点需要注意:
- 使用 SameSite 属性需要使用安全属性。如果 SameSite 属性没有安全属性,现代浏览器将自动拒绝cookie。
- 如果没有明确指定值,现代浏览器默认情况下会将 cookie 的 SameSite 属性设置为 Lax。Lax 允许在用户跟随链接时在请求中发送 cookie。
将 Cookie 限制为第一方请求还可以防止 ASP.NET Core web 应用受到跨站点攻击和漏洞,如跨站点请求伪造(CSRF)。
笔记
一个合理的警告:如果您的 ASP.NET Core web 应用与第三方网站广泛集成,并且这些网站需要使用您的 Cookie,则由于StrictSameSite 属性的限制性,用户可能会遇到一些故障。
根据您的 web 应用的风险级别,您可能希望将限制降低到SameSiteMode.Lax。
使用内容安全策略
web 安全生态系统的核心是我们每天用来与 ASP.NET Core web 应用(现代浏览器)交互的软件。浏览器具有内置的安全机制来保护用户免受攻击,从而使整个用户体验免受基于 web 的漏洞的攻击。此外,我们如何在 web 应用中编写代码对于指导浏览器如何启用这些安全功能至关重要。
在第 7 章安全配置错误的修复禁用安全功能配方中,我们了解到我们可以发送特殊的 HTTP 响应头来触发安全功能并告诉浏览器如何操作。我们可以告诉浏览器哪些主机可以安全地从中提取资源,以及在哪里可以安全地执行脚本。这些白名单规则可以使用CSP定义。
在本教程中,我们将学习如何实现基本的 CSP,以便我们可以将浏览器检索 web 资源的位置列为白名单。
准备好了吗
我们将使用上一个配方中使用的在线银行应用。使用 Visual Studio 代码,打开位于\Chapter13\content-security-policy\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
打开
Startup.cs查看Configure中的Use方法调用。中间件在修复禁用的安全特性配方app.Use(async (context, next) => { context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); context.Response.Headers.Add("X-Content-Type- Options", "nosniff"); context.Response.Headers.Add("X-Frame-Options", "DENY"); await next(); }); app.UseHttpsRedirection(); app.UseStaticFiles();中添加了我们在第 7 章、安全配置错误中了解的 HTTP 安全头
-
Let's add an additional layer of defense by adding a CSP to the collection of HTTP response headers:
app.Use(async (context, next) => { context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); context.Response.Headers.Add("X-Content-Type- Options", "nosniff"); context.Response.Headers.Add("X-Frame-Options", "DENY"); string scriptSrc = "script-src 'self' https://code.jquery.com;"; string styleSrc = "style-src 'self' 'unsafe-inline';"; string imgSrc = "img-src 'self' https://www.packtpub.com/;"; string objSrc = "object-src 'none'"; string defaultSrc = "default-src 'self';"; string csp = $"{defaultSrc}{scriptSrc}{styleSrc} {imgSrc}{objSrc}"; context.Response.Headers.Add($"Content-Security- Policy", csp); await next(); });在这里,我们定义了字符串变量来保存每个最常见的 CSP 头及其源位置。然后,我们将它们作为
Content-Security-PolicyHTTP 头添加到 HTTP 响应头集合中。
验证内容安全策略 HTTP 头
按照以下步骤验证 CSP 现在是否作为 HTTP 响应的一部分发送:
-
从菜单中导航到终端****新终端,或在 Visual Studio 代码中按Ctrl+Shift+即可。
** 在终端中键入以下命令以构建并运行示例应用:dotnet run- 打开浏览器并转到
https://localhost:5001/。* Log in using the following credentials:
a) 电邮:
stanley.s.jobson@lobortis.cab) 密码:
rUj5jtV8jrTyHnx!- Once authenticated, you will be redirected to the home page:
![Figure 13.1 – Home page authenticated]()
图 13.1–主页验证
- 按F12打开浏览器的开发者工具。* 进入网络选项卡,在流量列表中选择第一条 HTTP 流量。* 选择 HTTP 通信形式后,滚动右侧窗格以查看相应的 HTTP 响应安全标头:*
- 打开浏览器并转到
*
图 13.2–内容安全策略
通过这样做,安全头将被添加为我们的示例网上银行 web 应用发送的 HTTP 响应的一部分。
它是如何工作的…
每个字符串变量对应一个 CSP 头。
scriptSrc定义要从中加载 JavaScript 的可信源位置列表。self值属于我们的样本网上银行 web 应用本身和主机(https://code.jquery.com 我们下载 jQuery 库的地方:
string scriptSrc = "script-src 'self' https://code.jquery.com;";
styleSrc定义用于加载样式表的可信源位置列表。我们在这里使用self表示可以将样式表本地加载到我们的 web 应用,但我们也使用unsafe-inline允许使用内联<style>元素。使用unsafe-inline通常是不安全的,但由于没有用户控制的输入会影响我们样本网上银行 web app 的样式表,我已批准其使用,如下所示:
string styleSrc = "style-src 'self' 'unsafe-inline';";
笔记
我们不能说CSS 注入不会发生。示例 web 应用的未来版本可能允许用户控制的数据管理样式表属性。
有关 CSS 注入的更多信息,请访问OWASP Web 安全测试指南(WSTG中的CSS 注入测试部分:
接下来,我们必须为img-srcCSP 头定义可接受的值。我们可以添加https://www.packtpub.com/ 主机作为我们图像的可信来源:
string imgSrc = "img-src 'self' https://www.packtpub.com/;";
要获得安全的 CSP,object-src需要设置为none。此 CSP 标题将列出可接受的插件来源,并保留用于遗留小程序,这是一种过时的技术。不放置此标题表示谷歌 CSP 验证器对不安全的严重性很高(有关此工具的更多信息,请参见本配方的部分,还有更多…部分):
string objSrc = "object-src 'none'";
最后,default-srcCSP 头是所有其他未定义的 CSP 头的回退。
string defaultSrc = "default-src 'self';";
string csp = $"{defaultSrc}{scriptSrc}{styleSrc} {imgSrc}{objSrc}";
context.Response.Headers.Add($"Content-Security-Policy", csp);
前面的代码还使用字符串插值连接 CSP 头,并将它们添加到 HTTP 响应头集合中。
还有更多…
如果需要在页面中放置内联脚本和样式,则必须包含一个nonce作为script-src或style-src的源。此加密字符串在每个请求中必须是唯一的,以确保恶意参与者不会注入通过 ASP.NET Core web 应用中的 XSS 漏洞执行和呈现的内联脚本或样式。要快速启用此安全功能并减轻生成不可用的 nonce 的任务,请通过执行以下步骤使用NWebsec TagHelpers for ASP.NET Core:
-
安装必要的软件包:
dotnet add package NWebsec.AspNetCore.Mvc.TagHelpers -
Import the
NWebSectag helpers into_ViewImports.cshtml:@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, NWebsec.AspNetCore.Mvc.TagHelpers然后,在您的
script或style标签中使用nws-csp-add-nonce助手:@model IndexModel @{ ViewData["Title"] = "Home page"; } <style nws-csp-add-nonce="true"></style> <script nws-csp-add-nonce="true"> console.log('nonce added'); </script> this in turn will render the following markup <style nonce="QqTsxf7Oqqu0GOLPa36y21IV"></style> <script nonce="HlbkIsOBsmI66GXwH5635KBk"> console.log('nonce added'); </script>
此外,我们在本配方中使用的 CSP 标头并不全面。要了解更多关于其他 CSP 头的信息,请阅读Mozilla 开发者网络(MDN)网站上的CSP文档 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 。
您可以使用在线工具生成自己的 CSP。Report URI CSP Generator 允许您简单地勾选要包含在 CSP 中的 CSP 标题,并在文本框中填入可接受的主机,如下所示:

图 13.3–报告 URI CSP 生成器
这是在线工具的链接:https://report-uri.com/home/generate 。
另一个可以用来验证生成的 CSP 安全性的优秀工具是谷歌的 CSP 验证器,可在上找到 https://csp-evaluator.withgoogle.com/ :

图 13.4–谷歌的 CSP 验证程序
CSP 验证器由Lukas Weichselbaum开发,他是谷歌员工信息安全工程师之一。使用此工具,您可以针对不同版本的 CSP 验证您放入框中的策略,并验证您的 CSP 是否具有针对跨站点脚本(XSS的强防御能力。
修复剩余调试代码
调试和测试代码是构建 ASP.NET Core web 应用整个周期的一部分。NET 平台提供了大量用于调试的库和组件,包括有效的诊断和跟踪。但是,对于开发人员来说,让调试代码保持原样而忽略从存储库中删除代码或添加条件检查是一种不好的做法,从而导致在生产环境中部署不必要的代码。
在这个配方中,我们将学习如何确保应用环境验证来验证是否调用了必要的调试方法。
准备好了吗
我们将使用上一个配方中使用的在线银行应用。使用 Visual Studio 代码,打开位于\Chapter13\leftover-debug-code\before\OnlineBankingApp\的示例网上银行应用文件夹。
怎么做…
让我们来看看这个食谱的步骤:
-
在起始练习文件夹中,通过键入以下命令启动 Visual Studio 代码:
code . -
Open
Startup.csand, atConfigure, notice the call to the following function:app.UseStatusCodePages( "text/plain", "Status code page, status code: {0}");UseStatusCodePages是一个中间件,提供status code作为响应。这有助于确定我们的示例网上银行应用返回了什么状态代码,但这些状态代码对我们的用户没有用处。 -
In the same
Startup.csfile, notice the call to the following statement:services.AddDatabaseDeveloperPageExceptionFilter();AddDatabaseDeveloperPageExceptionFilter是一个异常过滤器,提供与数据库操作相关的异常详细信息。此方法对于调试和查找 DB 相关问题的解决方案非常有用。但是,不能在生产环境中调用它。 -
为了防止在生产环境中执行这些调试方法,请在进行这些调用之前添加一个检查,以验证当前上下文是否正在开发环境中运行:
if (Environment.IsDevelopment()) { services.AddDatabaseDeveloperPageExceptionFilter(); } -
UseStatusCodePagesis also placed in a code block that will only be invoked when the environment is in development due to theIsDevelopment()method:if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages( "text/plain", "Status code page, status code: {0}"); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); }这可以防止我们的示例网上银行 web 应用在发生意外错误时泄露敏感信息。
它是如何工作的…
IsDevelopment方法检查代码是否在开发环境的上下文中运行。在我们执行调试方法(如UseStatusCodePages和AddDatabaseDeveloperPageExceptionFilter之前,此方法是一个有用的测试。
当被数据库错误调用时,AddDatabaseDeveloperPageExceptionFilter可能会暴露敏感信息。作为开发周期的一部分,定期执行安全代码审查和执行静态应用安全测试有助于及早发现这些问题。
还有更多…
安全软件开发是一个广泛的主题,编写安全代码只是这个实践的一个子集。每个 ASP.NET Core web 开发人员必须确保在代码中应用安全性的基本原则(机密性、完整性、以及可用性)。编写代码时要牢记这些安全性要求。
虽然本章讨论了如何修复安全问题和 bug,但我们在这里学到的技术可以用于在过程的早期主动检查代码和搜索漏洞。提前检测代码中的安全问题,并让左移文化有助于开发人员进一步降低风险,从而在每个周期结束时产生更具攻击弹性的 ASP.NET Core web 应用。*
a-zA-Z0-9 ↩︎




















































浙公网安备 33010602011771号