使用本单元可以:
-
设计安全的 ASP.NET 页和控件。
-
使用正则表达式和其他技术开发安全的验证代码。
-
防止跨站点脚本攻击 (XSS)。
-
对用户进行身份验证和授权。
-
开发安全的窗体身份验证。
-
防止丰富的异常详细信息到达客户端。
-
管理和保护 ASP.NET 会话。
-
防止参数操作。
-
了解哪些对策适用于应对常见的威胁,包括代码注入、会话劫持、标识欺骗、参数操作、网络侦听、信息泄漏、跨站点脚本攻击 (XSS) 和 cookie 重放攻击。
适用范围
本单元适用于下列产品和技术:
-
Microsoft® Windows® Server 2000 和 Windows Server™ 2003 操作系统
-
Microsoft .NET Framework 1.1 和 ASP.NET 1.1
如何使用本单元
除了所提出的安全编程实践之外,还应该使用本指南中相应的单元辅助安全 ASP.NET 页和控件的构建。
-
实现“保护 ASP.NET 应用程序的安全”单元中的步骤。此单元有助于用 Machine.config 和 Web.config 中的安全设置适当地对 ASP.NET 进行配置。
-
使用配套的核对表“核对表:保护 ASP.NET 的安全”。它将本单元中的推荐实践与“保护 ASP.NET 应用程序的安全”结合起来。确保您实现本指导。
-
理解特定于 ASP.NET 页和控件的威胁和攻击。按照本单元中的准则应用对策。
-
阅读“安全 Web 应用程序的设计准则”单元。本单元中的许多推荐实践就是基于此单元中讨论的设计准则。
-
架构师应该使用本单元中的“设计注意事项”部分。
-
开发人员应该将本单元中的指导应用于其开发过程。开发人员必须特别注意对输入数据的验证,因为大多数主要的应用程序级攻击都依赖这个区域中的漏洞。
-
从编程的角度理解控件,对 ASP.NET 页和控件安全进行精细调整。
-
使用应用程序漏洞类别作为一种解决常见问题的办法。应用程序漏洞类别提供了着手处理和分类问题的有用方式。
本页内容
本单元概要
目标
适用范围
如何使用本单元
威胁与对策
设计注意事项
输入验证
跨站点脚本攻击
身份验证
授权
模拟
敏感数据
会话管理
参数操作
异常管理
审核和日志记录
小结
其他资源
威胁与对策
大多数 Web 应用程序攻击都需要在 HTTP 请求中传入恶意输入。 通常的目的是强制应用程序执行未授权的操作或者破坏其正常操作。这就是为什么彻底的输入验证是许多攻击的核心对策,而且它应该在开发 ASP.NET Web 页和控件时被置于最高优先级的原因。最大的威胁包括:
-
代码注入
-
会话攻击
-
标识欺骗
-
参数操作
-
网络侦听
-
信息泄漏
图 1 突出显示了 Web 应用程序最常见的威胁。
代码注入
当攻击者使用应用程序的安全上下文使任意代码运行时就发生了代码注入。如果您的应用程序使用特权帐户运行,风险将大大增加。
攻击
代码注入攻击有很多种类型。这些类型包括:
-
跨站点脚本攻击。向 Web 应用程序发送恶意脚本作为输入。它将传回用户的浏览器,在那里执行。
-
缓冲区溢出。验证托管代码的类型安全能够显著减少风险,但是您的应用程序仍然是有漏洞的,尤其是在它调用非托管代码的时候。缓冲区溢出能够允许攻击者在您的 Web 应用程序进程中使用安全上下文执行任意代码。
-
SQL 注入。这种攻击的目标是有漏洞的数据访问代码。攻击者发送能够在数据库中更改预期查询或者执行全新查询的 SQL 输入。窗体身份验证登录页是常见的目标,因为要使用用户名称和密码查询用户存储区。
漏洞
能够导致成功代码注入攻击的漏洞包括:
-
脆弱或者遗漏的输入验证,或者依赖客户端输入验证
-
在 HTML 输出中包含未验证的输入
-
动态构造不使用类型化参数的 SQL 语句
-
使用特权过高的进程帐户和数据库登录
对策
以下对策可以用于防止代码注入:
-
验证输入,从而使攻击者无法注入脚本代码或者导致缓冲区溢出。
-
编码所有包含输入的输出。这能够防止有可能存在恶意的脚本标记被客户端的浏览器解释为代码。
-
使用接受参数的存储过程,防止恶意 SQL输入被数据库当作可执行语句处理。
-
使用最低特权进程和模拟帐户。这能够降低风险和减少攻击者设法使用应用程序的安全上下文执行代码时带来的破坏。
会话攻击
在攻击者捕获了一个身份验证标记并控制了另一个用户的会话时就会发生会话劫持。身份验证标记经常存储在 cookie 或者 URL 中。如果攻击者捕获了身份验证标记,他就能够将其与请求一起传输到应用程序。 应用程序会将请求与合法用户的会话关联起来,从而使攻击者能够获取对要求身份验证访问的应用程序受限区域的访问权限。攻击者然后使用合法用户的标识和特权。
漏洞
使您的 Web 页和控件容易遭到会话劫持的常见的漏洞包括:
-
URL 中的不受保护的会话标识符
-
将个性化 cookie 与身份验证 cookie 混合
-
身份验证 cookie 通过未加密的链接传递
攻击
会话劫持攻击包括:
-
cookie 重放。攻击者通过使用网络监视软件或者通过其他方式(例如,通过利用 XSS 脚本注入漏洞捕获身份验证 cookie。
-
查询字符串操作。恶意用户将更改 URL 查询字符串中显而易见的会话标识符。
对策
您可以使用以下对策防止会话劫持:
-
将个性化 cookie 和身份验证 cookie 分离。
-
只通过 HTTPS 连接传递身份验证 cookie。
-
不要在查询字符串中传递代表已经过身份验证的用户的会话标识符。
-
在进行关键操作(如下定单、转帐等等)之前,对用户再次进行身份验证。
标识欺骗
在恶意用户使用合法用户的标识从而能够访问应用程序时就会发生标识欺骗。
漏洞
使您的 Web 页和控件容易遭到标识欺骗攻击的常见漏洞包括:
-
通过未加密链路传递身份验证凭据
-
通过未加密链路传递身份验证 cookie
-
脆弱的密码和策略
-
用户存储区中脆弱的凭据存储区
攻击
标识欺骗攻击包括:
-
cookie 重放。攻击者通过使用网络监视软件或者使用 XSS 攻击窃取身份验证 cookie。攻击者然后将 cookie 发送到应用程序以获取欺骗访问。
-
强力密码攻击。攻击者反复尝试各种用户名称和密码组合。
-
字典攻击。在这种自动形式的强力密码攻击中,将尝试用字典中的每个词作为密码。
对策
您可以使用以下对策防止标识欺骗:
-
只通过 HTTPS 连接传输身份验证凭据和 cookie。
-
强制使用坚固的密码。正则表达式可以用于确保用户所提供的密码满足合适的复杂性需求。
-
在数据库中存储密码验证器。存储带有随机 salt 值的不可逆密码哈希值,以减少字典攻击的风险。
有关在数据库中存储密码哈希值和其他机密的更多信息,请参阅“构建安全的数据访问”单元。
参数操作
参数是通过网络从客户端传递到服务器的数据项。它们包括窗体字段、查询字符串、查看状态、cookie 和 HTTP 头。如果敏感数据或者用于在服务器上做出安全决策的数据是使用不受保护的参数传递的,您的应用程序将可能很容易发生信息泄漏或者未授权访问。
漏洞
能够导致参数操作的漏洞包括:
-
使用隐藏窗体字段或者包含敏感数据的查询字符串
-
通过未加密的连接传输包含安全性敏感数据的 cookie
攻击
参数操作攻击包括:
-
cookie 重放攻击。攻击者捕获并更改 cookie 然后将其重放给应用程序。这能够很容易地导致标识欺骗和特权提升,如果 cookie 中包含服务器上用于身份验证或者授权的数据。
-
隐藏窗体字段的操作。这些字段包含在服务器上用来进行安全决策的数据。
-
查询字符串参数的操作。
对策
您可以使用以下对策防止参数操作:
-
不要依赖客户端状态管理选项。避免使用任何客户端状态管理选项(例如查看状态、cookie、查询字符串或者隐藏窗体字段)来存储敏感数据。
-
将敏感数据存储在服务器上。使用会话标记将用户的会话与在服务器上维护的敏感数据项关联起来。
-
使用消息身份验证代码 (MAC) 保护会话标记。将这种方式与身份验证、授权和服务器上的业务逻辑结合,确保标记不会重放。
网络侦听
网络侦听往往要使用网络监视软件跟踪在浏览器和 Web 服务器之间发送的数据包。这能够导致特定于应用程序的机密数据的泄漏、登录凭据的检索或者身份验证 cookie 的捕获。
漏洞
能够导致成功的网络侦听的漏洞包括:
-
发送敏感数据时缺乏加密
-
通过未加密的信道发送身份验证 cookie
攻击
网络侦听攻击是通过使用置于网络上捕获流量的数据包嗅探工具来执行的。
对策
要对抗网络侦听,应该使用安全套接字层 (SSL) 协议在浏览器和 Web 服务器之间提供加密的通信信道。使用 SSL 是必需的,无论通过网络发送的是凭据、身份验证票证或者敏感的应用程序数据。
信息泄漏
在攻击者探测您的 Web 页以寻找导致异常情形的方法时就会发生信息泄漏。这对于攻击者而言是一种成效显著的手段,因为异常详细信息(经常以 HTML 返回并显示在浏览器中)能够泄漏极为有用的信息,例如包含数据库连接字符串的堆栈跟踪信息、数据库名称、数据库架构信息、SQL 语句和操作系统及平台版本。
漏洞
能够导致信息泄漏的漏洞包括:
-
脆弱的异常处理
-
允许原始异常详细信息传播到客户端
攻击
能够导致信息泄漏的攻击有许多种。这些攻击包括:
-
缓冲区溢出。
-
故意发送格式错误的输入。
对策
为了防止信息泄漏,应该:
-
使用结构化异常处理。
-
返回一般性错误页到客户端。
-
使用包含一般和无害错误消息的默认重定向页。
设计注意事项
在您开发 Web 页和控件之前,有许多重要的问题应该在设计时考虑。以下是关键的注意事项:
-
使用服务器端输入验证。
-
将 Web 站点分区。
-
考虑用于资源访问的标识。
-
保护凭据和身份验证票证。
-
安全地失败。
-
考虑授权粒度。
-
将 Web 控件和用户控件置于不同的程序集中。
-
将资源访问代码置于不同的程序集中。
使用服务器端输入验证
在设计时,标识您的 Web 页和控件处理的用户输入的所有各种来源。这包括窗体字段、查询字符串和从 Web 用户接收的 cookie 以及来自后端数据源的数据。Web 用户显然不在您的应用程序的信任边界之内,因此所有来自此来源的输入都必须在服务器上进行身份验证。除非您可以绝对信任接收自后端数据源的数据,否则数据也应该进行验证和净化,才能发送到客户端。确保您的解决方案并不依赖客户端验证,因为这很容易绕过。
将 Web 站点分区
您的 Web 站点设计应该明确地区分可公开访问区域和要求经身份验证才能访问的受限区域。使用应用程序虚拟根目录之下不同的子目录维护受限页,例如在传统的电子商务 Web 站点中的结帐功能,就要求经身份验证的访问并传输敏感数据例如信用卡号码。不同的子目录使您能够应用更多安全性(例如,通过要求使用 SSL)而不会在整个站点产生 SSL 性能开销。它还使您能够通过限制身份验证 cookie 传输只使用 HTTPS 连接,从而减少会话劫持的风险。图 2 说明了典型的分区。
注意在图 2 中,受限子文件夹在 Internet 信息服务 (IIS) 中配置为要求 SSL 访问。Web.config 中的第一个 <authorization>元素允许所有用户访问公开区域,而第二个元素则防止未经身份验证的用户访问受保护子文件夹的内容并强制登录。
有关限制身份验证 cookie 从而使其只使用 HTTPS 连接传递,以及如何在受限和非受限页之间导航的更多信息,请参阅本单元“身份验证”部分中的“使用绝对 URL 进行导航”。
考虑用于资源访问的标识
默认时,ASP.NET 应用程序并不模拟,而且使用最低特权 ASPNET 进程帐户运行 ASP.NET Web 应用程序和进行资源访问。默认值是推荐配置。在几种情况下,可能需要使用不同的 Windows 安全上下文进行资源访问。这些情况包括:
-
在同一服务器上寄宿多个应用程序
您可以使用 IIS 配置每个应用程序以使用不同的匿名 Internet 用户帐户然后启用模拟。每个应用程序从而都有唯一标识进行资源访问。有关这种方式的更多信息,请参阅“寄宿多个 Web 应用程序”单元。
-
访问带有特定身份验证需求的远程资源
如果您需要访问特定远程资源(例如,文件共享)并且已经提供了特殊的 Windows 帐户,可以配置这个帐户为应用程序的匿名 Web 用户帐户。然后可以在访问特定远程资源之前使用编程模拟。有关更多信息,请参阅本单元后面的“模拟”。
保护凭据和身份验证票证
您的设计应该包括如何保护凭据和身份验证票证。凭据在跨网络传递时以及持久存储在存储区(例如配置文件中)时需要进行保护。身份验证票证必须在跨网络时进行保护,因为它们容易遭到劫持。加密提供了一种解决方案。SSL 或者 IPSec 可以用于保护网络上的凭据和票证,而 DPAPI 为加密配置文件中的凭据提供了一种很好的解决方案。
安全地失败
如果您的应用程序由于某个不可恢复的异常情形而失败了,确保它将安全地失败,而且不会使系统门户大开。确保不允许对恶意用户有价值的异常详细信息传播到客户端,而是返回一般性错误页。计划使用结构化异常处理来处理错误,而不要依赖方法的错误代码。
考虑授权粒度
考虑在站点的身份验证部分中使用的授权粒度。如果您配置了一个要求身份验证的目录,则所有用户都将有相同的对该目录中页的访问权限吗?如果必要,您可以通过使用不同 <location> 元素中的多个<authorization> 元素,根据标识或调用方的角色成员资格(后者更加常见)为不同的页应用不同的授权规则。
例如,同一目录中的两个页在 Web.config 中可有不同的 <allow> 和 <deny> 元素。
将 Web 控件和用户控件置于不同的程序集中
当 Web 控件和用户控件置于各自的程序集中,您可以通过使用代码访问安全策略独立地为每个程序集配置安全性。这为管理员提供了更大的灵活性,而且这意味着您用不着只是为了满足一个控件的需求,而非要给所有控件授予扩展权限。
将资源访问代码置于不同的程序集中
使用不同的程序集,而且从页类调用它们,而不是将资源访问代码嵌入页类的事件处理程序中。这为代码访问安全策略提供更大的灵活性,而且对于构建部分信任 Web 应用程序尤其重要。有关更多信息,请参阅单元“在 ASP.NET 中使用代码访问安全”
输入验证
如果您没有任何限制地接受输入的类型、长度、格式或者范围,那么您的应用程序不可能是可靠的。如果攻击者发现您对输入会没有任何限制地接受,输入验证将成为安全问题。攻击者然后会提供仔细编写的输入危及应用程序的安全。对用户输入的错误信任是 Web 应用程序中最常见和最具破坏性的漏洞。
约束、然后净化
首先通过验证类型、长度、格式和范围约束输入和检查数据是否正常。有时还需要净化输入和使有可能存在恶意的输入安全。例如,如果您的应用程序支持自由格式的输入字段,例如注释字段,可能需要允许一些“安全”的 HTML 元素,例如<b>,并剥离其他的所有 HTML 元素。以下表总结了约束和净化数据可以采用的选择:
表 1 约束和净化数据的选择 要求 | 选择 |
---|---|
类型检查 | .NET Framework 类型系统。分析字符串数据,转换为强类型,然后处理 FormatExceptions。 |
长度检查 | 使用正则表达式的 |
格式检查 | 使用正则表达式对 |
范围检查 | ASP.NET RangeValidator 控件(支持货币、日期、整数、双精度和字符串数据) |
正则表达式
您可以使用正则表达式限制有效字符的范围,以删除不希望的字符并执行长度和格式检查。您可以通过定义输入必须匹配的模式约束输入格式。ASP.NET 提供了 RegularExpressionValidator 控件,而 Regex 类可以从 System.Text.RegularExpressions 命名空间获得。
如果您使用验证程序控件,验证将在控件为空时通过。对于必需的字段,使用 RequiredFieldValidator。同样,正则表达式验证实现在客户端和服务器上也有微小的差异。在客户端,使用 Microsoft JScript_ 开发软件的正则表达式语法。在服务器,使用 System.Text.RegularExpressions.Regex 语法。因为 JScript 正则表达式语法是 System.Text.RegularExpressions.Regex 语法的子集,推荐使用 JScript 正则表达式语法,这样能够在客户端和服务器上得到同样的结果。
有关 ASP.NET 验证程序控件完整范围的更多信息,请参考 .NET Framework 文档。
RegularExpressionValidator 控件
要验证 Web 窗体的字段输入,您可以使用 RegularExpressionValidator 控件。将控件拖到 Web 窗体,并设置其 ValidationExpression、ControlToValidate 和 ErrorMessage 属性。
您可以使用 Microsoft Visual Studio_ .NET 中的属性窗口设置验证表达式,也可以在 Page_Load 事件处理程序中动态地设置属性。后一种方法使您能够将页中所有控件的所有正则表达式组合起来。
Regex Class
如果您使用不带 runat="server" 属性(不使用 RegularExpressionValidator 控件)的常规 HTML 控件,或者需要验证来自其他来源(例如查询字符串或者 cookie)的输入,可以在页类中或者验证辅助方法中(可能在不同的程序集中)使用 Regex 类。本部分后面举出了一些例子。
正则表达式注释
如果您使用以下语法,并使用 # 注释表达式的每个组成部分,正则表达式就容易理解多了。要使用注释,您还必须指定 RegexOptions.IgnorePatternWhitespace,这意味着忽略非转义空格。
Regex regex = new Regex(@" ^ # anchor at the start (?=.*/d) # must contain at least one digit (?=.*[a-z]) # must contain one lowercase (?=.*[A-Z]) # must contain one uppercase .{8,10} # From 8 to 10 characters in length $ # anchor at the end", RegexOptions.IgnorePatternWhitespace);
字符串字段
要验证字符串字段,例如姓名、地址、税务标识号码等等,可以使用正则表达式进行以下操作:
-
约束输入字符的可接受范围。
-
应用格式规则。例如,基于模式的字段如税务标识号码、邮政编码,要求输入字符采用特定模式。
-
检查长度。
姓名
以下示例说明了 RegularExpressionValidator 控件如何用于验证姓名字段。
<form id="WebForm" method="post" runat="server"> <asp:TextBox id="txtName" runat="server"></asp:TextBox> <asp:RegularExpressionValidator id="nameRegex"runat="server" ControlToValidate="txtName" ValidationExpression="^[a-zA-Z'.`-´/s]{1,40}$" ErrorMessage="Invalid name"> </asp:regularexpressionvalidator> </form>
前面的验证表达式将输入姓名字段约束为字母字符(小写字母和大写字母),某些姓名中的一个撇号字符,例如 O'Dell,和句点符。此外,字段长度约束为 40 个字符。
社会安全号
以下示例说明了为用于验证美国社会安全号窗体字段的 RegularExpressionValidator 控件所生成的 HTML 代码:
<form id="WebForm" method="post" runat="server"> <asp:TextBox id="txtSSN" runat="server"></asp:TextBox> <asp:RegularExpressionValidator id="ssnRegex" runat="server" ErrorMessage="Invalid social security number" ValidationExpression="/d{3}-/d{2}-/d{4}" ControlToValidate="txtSSN"> </asp:RegularExpressionValidator> </form>
前面的验证表达式是 Visual Studio .NET 提供的标准表达式之一。它验证了所提供的输入字段的格式及其类型和长度。输入必须包含三位数加一个短划线,然后是两个数后跟一个短划线,然后是四个数。
如果您不使用服务器控件(不使用验证程序控件),或者需要验证来自窗体字段之外来源的输入,可以在方法代码中使用 System.Text.RegularExpression.Regex 类。以下示例说明了如何通过直接在页类中使用静态 Regex.IsMatch 方法而不是使用验证程序控件,来验证同一字段:
if (!Regex.IsMatch(txtSSN.Text, @"^/d{3}-/d{2}-/d{4}$")) { // Invalid Social Security Number }
日期字段
有等效 .NET Framework 类型的输入字段可以通过 .NET Framework 类型系统进行类型检查。例如,要验证日期,可以将输入值转换为 System.DateTime 类型的一个变量,并在输入数据不兼容的情况下处理任何生成的格式异常,如下所示。
try { DateTime dt = DateTime.Parse(txtDate.Text).Date; } // If the type conversion fails, a FormatException is thrown catch( FormatException ex ) { // Return invalid date message to caller }
除了格式和类型检查,您可能还需要对日期字段执行范围检查。这可以使用 DateTime 变量很容易地执行,如下所示。
// Exception handling is omitted for brevity DateTime dt = DateTime.Parse(txtDate.Text).Date; // The date must be today or earlier if ( dt > DateTime.Now.Date ) throw new ArgumentException("Date must be in the past");
数字字段
如果您需要验证数字数据例如年龄,可以使用 int 类型执行类型检查。要将字符串输入转换为整数形式,您可以使用 Int32.Parse 或者 Convert.ToIn32,然后处理由于非法数据类型而生成的任何 FormatException,如下所示:
try { int i = Int32.Parse(txtAge.Text); . . . } catch( FormatException) { . . . }
范围检查
有时您需要验证输入数据是否处于预先确定的范围中。以下代码使用了一个 ASP.NET RangeValidator 控件将输入约束为 0 和 255 之间的整数。这个例子还使用了 RequiredFieldValidator。除 RequiredFieldValidator 之外,其他验证程序控件接受空白输入。
<form id="WebForm3" method="post" runat="server"> <asp:TextBox id="txtNumber" runat="server"></asp:TextBox> <asp:RequiredFieldValidator id="rangeRegex" runat="server" ErrorMessage="Please enter a number between 0 and 255" ControlToValidate="txtNumber" style="LEFT: 10px; POSITION: absolute; TOP: 47px" > </asp:RequiredFieldValidator> <asp:RangeValidator id="RangeValidator1" runat="server" ErrorMessage="Please enter a number between 0 and 255" ControlToValidate="TextBox1" Type="Integer" MinimumValue="0" MaximumValue="255" style="LEFT: 10px; POSITION: absolute; TOP: 47px" > </asp:RangeValidator> <asp:Button id="Button1" style="LEFT: 10px; POSITION: absolute; TOP: 100px" runat="server" Text="Button"></asp:Button> </form>
以下示例说明了如何使用 Regex 类验证范围:
try { // The conversion will raise an exception if not valid. int i = Convert.ToInt32(sInput); if ((0 <= i && i <= 255) == true) { // data is valid, use the number } } catch( FormatException ) { . . . }
净化输入
所谓净化就是使有可能存在的恶意数据变得安全。在允许的输入范围无法保证输入安全时,这是非常有用的。净化可能包括删除用户所提供的字符串最后的空白,或者将值转义使它们当作文本处理。如果您需要净化输入,转换或者删除特定的输入字符,可以使用 Regex.Replace。
注 使用这种方式可以进行纵深防范。应该总是首先约束输入为已知“正常”值的集合。
以下代码删除了包括 <>/"'%;()& 在内的一组有可能不安全的字符。
private string SanitizeInput(string input) { Regex badCharReplace = new Regex(@"^([<>""'%;()&])$"); string goodChars = badCharReplace.Replace(input, ""); return goodChars; }
有关净化自由格式输入字段的更多信息,例如注释字段,请参阅本单元后面的“跨站点脚本攻击”中的“净化自由格式的输入”。
验证 HTML 控件
如果您不使用服务器控件(即带有 runat="server" 属性的控件)而是使用常规的 HTML 控件,则无法使用 ASP.NET 验证程序控件。相反您可以通过在 Page_Load 事件处理程序中使用正则表达式,以验证 Web 页的内容,如下所示。
using System.Text.RegularExpressions; . . . private void Page_Load(object sender, System.EventArgs e) { // Note that IsPostBack applies only for // server forms (with runat="server") if ( Request.RequestType == "POST" ) // non-server forms { // Validate the supplied email address if( !Regex.Match(Request.Form["email"], @"^/w+([-+.]/w+)*@/w+([-.]/w+)*/./w+([-.]/w+)*$", RegexOptions.None).Success) { // Invalid email address } // Validate the supplied name if ( !RegEx.Match(Request.Form["name"], @"^[A-Za-z'/- ]$", RegexOptions.None).Success) { // Invalid name } } }
验证用于数据访问的输入
如果您根据用户输入生成动态的 SQL 查询,SQL 注入攻击能够注入可以在数据库中执行的恶意 SQL 命令。在典型的基于 Web 的数据访问场景中,可以使用以下纵深防范策略:
-
使用正则表达式约束页类中的输入。
-
净化或者拒绝输入。为了纵深防范,您可以选择使用辅助方法删除空字符或者其他已知的不良字符。
-
使用参数化存储过程进行数据访问,从而确保对 SQL 查询中使用的数据执行类型和长度检查。
有关使用参数进行数据访问和编写安全的数据访问代码的更多信息,请参阅“构建安全的数据访问”单元。
验证用于文件 I/O 的输入
一般而言,您应该避免编写从调用方接受文件输入或者路径输入的代码。相反,在读取和写入数据时应该使用固定文件名和位置。这能够确保您的代码无法被强制访问任意文件。还能确保代码不容易遇到规范化错误。
如果您确实需要接受输入文件名,有两个主要的挑战。首先,生成的文件路径和名称是有效文件系统名称吗?其次,路径在您的应用程序的上下文中是有效的吗?例如,它在应用程序的虚拟根目录之下吗?
要规范化文件名,应该使用 System.IO.Path.GetFullPath。要检查文件路径是否在应用程序的上下文中有效,您可以使用 .NET 代码访问安全给代码授予精确的 FileIOPermission,从而代码只能访问来自特定目录的文件。有关更多信息,请参阅“构建安全的程序集”和“代码访问安全实践”单元中的“文件 I/O”部分。
使用 MapPath
如果您使用 MapPath 将所提供的虚拟路径映射到服务器上的一个物理路径,可以使用接受 bool 参数的 Request.MapPath 的负载,从而可以防止跨应用程序的映射,如下所示:
try { string mappedPath = Request.MapPath( inputPath.Text, Request.ApplicationPath, false); } catch (HttpException) { // Cross-application mapping attempted }
最后的 false 参数防止跨应用程序映射。这意味着用户无法成功地提供包含 ".." 的路径从而遍历到应用程序的虚拟目录层次之外。任何这样做的企图,都会导致 HttpException 类型的异常。
注 服务器控件能够使用 Control.MapPathSecure 方法读取文件。这个方法要求通过代码访问安全策略授予呼叫代码完全信任;否则将引发一个 HttpException。有关更多信息,请参阅 .NET Framework SDK 文档中的 Control.MapPathSecure。
常见的正则表达式
Visual Studio .NET 提供了一组有用的正则表达式。要访问它们,将 RegularExpresssionValidator 控件添加到 Web 窗体,并单击控件 Expression 属性字段中的省略按钮。下表列出了常用的 Web 页字段的其他一些有用的表达式。
表 2 有用的正则表达式字段 字段 | 表达式 | 格式示例 | 说明 |
---|---|---|---|
Name | [a-zA-Z'`-´/s]{1,40} | John Doe | 验证名称。允许最多 40 个大写字母、小写字母字符和一些姓名中常见的特殊字符。这个列表可以进行调整。 |
Numbers | ^/D?(/d{3})/D?/D?(/d{3})/D?(/d{4})$ | (425)-555-0123 | 验证美国电话号码。 |
| /w+([-+.]/w+)*@/w+([-.]/w+)*/./w+([-.]/w+)* | 验证电子邮件地址。 | |
URL | ^(http|https|ftp)/://[a-zA-Z0-9/-/.]+/.[a-zA-Z]{2,3}(:[a-zA-Z0-9]*)?/?([a-zA-Z0-9/-/._/?/,/'+&%/$#/=~])*$ | 验证 URL。 | |
Zip Code | ^(/d{5}-/d{4}|/d{5}|/d{9})$|^([a-zA-Z]/d[a-zA-Z] /d[a-zA-Z]/d)$ | 验证美国邮政编码,允许 5 位或者 9 位数。 | |
Password | ^(?=.*/d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ | 验证坚固的密码。必须在 8 个到 10 个字符之间。必须包含大写字母、小写字母和数字组合,没有特殊字符。 | |
非负整数 | /d+ | 0 | 验证大于零的整数。 |
Currency (非负) | "/d+(/./d/d)?" | 验证正货币值。要求小数点后两位。 | |
Currency (正或者负) | "(-)?/d+(/./d/d)?" | 验证正或者负货币值。要求小数点后两位。 |
跨站点脚本攻击
XSS 攻击通过注入客户端脚本代码利用 Web 页验证中的漏洞。这个代码然后会发送回不加怀疑的用户,由浏览器来执行。因为浏览器是从可信站点下载脚本代码,浏览器没有办法标识代码是否合法,而且 Internet Explorer 安全区域也没有提供任何防范。XSS 攻击还能够通过 HTTP 或者 HTTPS (SSL) 连接工作。当一个攻击者编写脚本检索能够提供对可信站点访问权限的身份验证 cookie ,并将它张贴到攻击者知道的 Web 地址,将出现最严重的问题。这允许攻击者欺骗合法用户的标识,并获取访问 Web 站点的违法权限。使用以下对策防止 XSS 攻击:
-
验证输入
-
编码输出
验证输入
使用本单元中前面叙述的各种技术验证来自应用程序的信任边界之外的任何输入的类型、长度、格式和范围。
编码输出
如果您将文本输出写入 Web 页,而且您不能肯定地知道文本中是否不包含 HTML 特殊字符(例如 <、 > 和 & ),则应该确保使用 HttpUtility.HtmlEncode 方法对其进行预处理。即使文本来自用户输入、数据库或者本地文件,也应该这样做。类似地,使用 HttpUtility.UrlEncode 编码 URL 字符串。
HtmlEncode 方法将 HTML 中有特殊意义的字符用代表这些字符的 HTML 变量代替。例如, < 将用 < 代替,而 " 用 " 代替。编码的数据不会导致浏览器执行代码。相反,数据会以无害的 HTML 形式显示。
Response.Write(HttpUtility.HtmlEncode(Request.Form["name"]));
数据绑定控件
数据绑定 Web 控件不会对输出进行编码。唯一编码输出的控件是 TextBox 控件(当其 TextMode 属性设置为 MultiLine 时)。如果您将任何其他控件绑定到有恶意 XSS 代码的数据,代码将在客户端执行。因此,如果您从数据库中检索数据,而且无法确定数据是否有效(可能因为数据库是与其他应用程序共享的),在数据传回客户端之前对数据进行编码。
净化自由格式的输入
如果您的 Web 页包括自由格式文本框,例如“comments”字段,其中需要允许一些安全的 HTML 元素例如 <b> 和 <i>,可以通过首先用 HtmlEncode 进行预处理,然后有选择地删除所允许元素的编码,而安全地解决这一问题,如下所示:
StringBuilder sb = new StringBuilder( HttpUtility.HtmlEncode(userInput) ) ; sb.Replace("<b>", ""); sb.Replace("</b>", ""); sb.Replace("<i>", "<i>"); sb.Replace("</i>", ""); Response.Write(sb.ToString());
纵深防范对策
除了前面讨论的技术之外,还可以使用以下对策进行纵深防范以防止 XSS:
-
设置正确的字符编码。
-
使用 ASP.NET 1.1 版 validateRequest 选项。
-
在 Web 服务器上安装 URLScan。
-
使用 HttpOnly cookie 选项。
-
使用 <frame> 安全属性。
-
使用 innerText 属性。
设置正确的字符编码
要成功地对什么数据对于您的 Web 页有效进行限制,限制输入数据的表示方式非常重要。这防止了恶意用户使用规范化和多字节转义序列欺骗您的输入验证例程。
ASP.NET 使您能够在页级或者应用程序级通过使用 Web.config 中的 <globalization> 元素指定字符集。 两种方法都如下所示,其中使用了 ISO-8859-1 字符编码,这在早期 HTML 和 HTTP 版本中是默认选项。
在页级设置字符编码,使用 <meta> 元素或者 ResponseEncoding 页级属性,如下所示:
<meta http-equiv="Content Type" content="text/html; charset=ISO-8859-1" />
或者:
<% @ Page ResponseEncoding="ISO-8859-1" %>
在 Web.config 中设置字符编码,使用以下配置:
<configuration> <system.web> <globalization requestEncoding="ISO-8859-1" responseEncoding="ISO-8859-1"/> </system.web> </configuration>
验证 Unicode 字符
使用以下代码在页中验证 Unicode 字符:
using System.Text.RegularExpressions; . . . private void Page_Load(object sender, System.EventArgs e) { // Name must contain between 1 and 40 alphanumeric characters // together with (optionally) special characters '`´ for names such // as D'Angelo if (!Regex.IsMatch(Request.Form["name"], @"^[/p{L}/p{Zs}/p{Lu}/p{Ll}]{1,40}$")) throw new ArgumentException("Invalid name parameter"); // Use individual regular expressions to validate other parameters . . . }
以下解释了前面代码中显示的正则表达式:
-
{<name>} 指定命名 Unicode 字符类。
-
/p{<name>} 匹配通过 {<name>} 指定的命名字符类中的任何字符。
-
{L} 执行从左到右的匹配。
-
{Lu} 执行大写字母匹配。
-
{Ll} 执行小写字母匹配。
-
{Zs} 匹配分隔符和空格。
-
{1,40} 意味着字符数不小于 1,不大于 40。
-
{Mn} 匹配标记和非空格字符。
-
{Zs} 匹配分隔符和空格。
-
* 指定 0 或者更多匹配。
-
$ 意味着在此位置停止查看。
使用 ASP.NET validateRequest 选项
validateRequest 属性是 .NET Framework 1.1 版的功能。这个属性在 Machine.config 中的 <pages> 元素上默认时设置为 true。它指示 ASP.NET 检查所有从浏览器接收的数据是否有可能存在恶意的输入,例如,包含 <script> 元素的输入。ASP.NET 检查从 HTML 窗体字段、cookie 和查询字符串接收的输入。.NET Framework 1.0 版没有提供任何等效的功能, 但是 IIS URLScan Internet 服务器应用程序编程接口 (ISAPI) 筛选器能够执行类似的工作。您还可以使用 @ Page 标记将设置应用到每个页,如下所示:
<% @ Page validateRequest="True" %>
在 Web 服务器上安装URLScan
URLScan 是一个 ISAPI 筛选器,当您运行 IISLockdown 工具时安装它。它通过拒绝有可能存在恶意的输入,有助于减少 XSS 攻击的威胁。有关 IISLockdown 和 URLScan 的更多信息,请参阅“保护 Web 服务器的安全”单元。
注 Windows Server 2003 上的 IIS 6.0 具有等效于内置的 URLScan 工具的功能。
使用 HttpOnly Cookie 选项
Internet Explorer 6 Service Pack 1 支持新的 HttpOnly cookie 属性,这能够防止客户端脚本访问来自 document.cookie 属性的 cookie。相反它会返回一个空的字符串。无论何时用户浏览到当前域中的 Web 站点时,cookie 仍然会发送到服务器。
注 不支持 HttpOnly cookie 属性的 Web 浏览器要么忽略 cookie 要么忽略此属性,这意味着它仍然容易遭到 XSS 攻击。
System.Net.Cookie 类目前并不支持 HttpOnly 属性。要在 cookie 中添加一个 HttpOnly 属性,需要使用一个 ISAPI 筛选器,或者如果您需要托管代码解决方案,将以下代码添加到 Global.asax 中应用程序的 Application_EndRequest 事件处理程序中:
protected void Application_EndRequest(Object sender, EventArgs e) { string authCookie = FormsAuthentication.FormsCookieName; foreach (string sCookie in Response.Cookies) { // Just set the HttpOnly attribute on the Forms authentication cookie // Skip this check to set the attribute on all cookies in the collection if (sCookie.Equals(authCookie)) { // Force HttpOnly to be added to the cookie header Response.Cookies[sCookie].Path += ";HttpOnly"; } } }
注 .NET Framework 的未来版本可能在 Cookie 类中包含一个 HttpOnly 属性。
使用 安全属性
Internet Explorer 6 和更高版本支持 <frame> 和 <iframe> 元素的新的 security 属性。您可以使用 security 属性将用户的受限站点 Internet Explorer 安全区域设置应用到单独的 frame 或者 iframe。默认时,受限站点区域不支持脚本执行。如果您使用 security 属性,必须在当前设置其为“restricted”,如下所示:
<frame security="restricted" src="http://www.somesite.com/somepage.htm"></frame>
使用 innerText 属性
如果您用不可信输入创建页,使用 innerText 属性代替 innerHTML。innerText 属性将安全地提供内容,并确保脚本不会执行。
身份验证
脆弱的身份验证会增加标识欺骗威胁。如果用户的登录凭据落到了恶意用户的手中,攻击者将欺骗用户的标识并获取访问应用程序的权限。攻击者将共享应用程序中所有用户的特权。凭据在通过网络传输和持久存储(例如存储在应用程序的用户存储区中)时必须予以保护。代表一个对应用程序已进行身份验证标识的身份验证 cookie,在最初的登录之后必须也进行保护,以减少会话劫持和 cookie 重放攻击的风险。
窗体身份验证
会话劫持和 cookie 重放攻击的威胁对于使用窗体身份验证的应用程序尤其严重。在使用用户所提供的凭据查询数据库时,您必须特别小心,确保不容易遭到 SQL 注入攻击。此外,为了防止标识欺骗,应该确保用户存储区的安全,而且实施了坚固的密码。
以下片段说明了 Web.config 中的“安全的”窗体身份验证配置:
<forms loginUrl="Restricted/login.aspx" Login page in an SSL protected folder protection="All" Privacy and integrity requireSSL="true" Prevents cookie being sent over http timeout="10" Limited session lifetime name="AppNameCookie" Unique per-application name path="/FormsAuth" and path slidingExpiration="true" > Sliding session lifetime </forms>
以下推荐实践有助于构建安全的窗体身份验证解决方案:
-
为 Web 站点分区。
-
使用 SSL 保护受限页。
-
使用 URL 授权。
-
保护身份验证 cookie。
-
使用绝对 URL 进行导航。
-
使用安全的凭据管理。
为 Web 站点分区
在站点设计中,一定要将要求身份验证访问权限的安全页置于与可匿名访问的页不同的子目录中。图 3 说明了 Visual Studio .NET 解决方案资源管理器窗口的典型安排。请注意窗体登录页是怎样与其他受限页置于不同子目录中的。
注 如果您在应用程序中使用 Server.Transfer 从一个匿名页传输到安全页,.NET Framework 1.1 版或者更早的版本将绕过身份验证检查,因此对使用 Server.Transfer 的代码应该进行验证,以确保它不会传输到安全的目录。
用 SSL 保护受限页
要确保使用 SSL 保护从登录窗体发送而来的登录凭据,以及保护通过受限页的后续请求传递的身份验证 cookie,请在 IIS 中配置安全的文件夹以要求使用 SSL。这将设置 IIS 元数据库中文件夹的 AccessSSL=true 属性。对受保护文件夹中页的请求只有在请求 URL 中使用 https 时才会成功。
对于 SSL,必须在 Web 服务器上安装服务器证书。有关更多信息,请参阅“如何:在 Web 服务器上安装 SSL ”,在“ Microsoft patterns & practices 第 I 卷,构建安全的 ASP.NET Web 应用程序:身份验证、授权和安全通讯”的“如何……”部分中,网址是:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/secnetlpMSDN.asp。
使用 URL 授权
要允许匿名访问公开页,使用以下 <authorization> 元素。
<system.web> <!-- The virtual directory root folder contains general pages. Unauthenticated users can view them and they do not need to be secured with SSL. --> <authorization> <allow users="*" /> </authorization> </system.web>
使用以下 Web.config 中 <location> 元素中的 <authorization> 元素拒绝未经身份验证的用户的访问,并强制重定向到 <forms> 元素指定的登录页:
<!-- The restricted folder is for authenticated and SSL access only. --> <location path="Secure" > <system.web> <authorization> <deny users="?" /> </authorization> </system.web> </location>
保护身份验证 cookie
要防止会话劫持和 cookie 重放攻击,通过确保 cookie 只通过使用 HTTPS 协议的 SSL 连接进行传输来保护此 cookie。为了进一步降低风险,在将 cookie 发送到客户端之前对其加密,并限制 cookie 的有效时间。要保护身份验证 cookie:
-
限制身份验证 cookie 只使用 HTTPS 连接。
-
加密 cookie。
-
限制 cookie 生存期。
-
考虑使用固定到期时间。
-
不要持久存储身份验证 cookie。
-
使身份验证和个性化 cookie 分离。
-
使用唯一的 cookie 名称和路径。
限制身份验证 cookie 只使用 HTTPS 连接
cookie 支持能确定是否浏览器应该将 cookie 发送回服务器的“安全”属性。设置安全属性后, cookie 将由浏览器只发送到使用 HTTPS URL 请求的安全的页。
如果您使用 .NET Framework 1.1 版,通过使用 <forms> 元素的 requireSSL="true" 设置安全属性,如下所示:
<forms loginUrl="Secure/Login.aspx" requireSSL="true" . . . />
如果您使用 .NET Framework 1.0 版,使用以下代码在 Global.asax 的 Application_EndRequest 事件处理程序中手动设置安全属性:
protected void Application_EndRequest(Object sender, EventArgs e) { string authCookie = FormsAuthentication.FormsCookieName; foreach (string sCookie in Response.Cookies) { if (sCookie.Equals(authCookie)) { // Set the cookie to be secure. Browsers will send the cookie // only to pages requested with https Response.Cookies[sCookie].Secure = true; } } }
加密 cookie
应该加密 cookie 内容,即使您使用 SSL。如果他或者她设法通过 XSS 攻击窃取 cookie,这能够防止攻击者查看或者修改 cookie。在这种情况下,攻击者仍然能够使用 cookie 获取对您的应用程序的访问。减少这种风险的最佳方式是实现适当的对策以防止 XSS 攻击(在本单元前面的“跨站点脚本攻击”叙述),并如下面的推荐实践中叙述的那样限制 cookie 的生存期。
要提供 cookie 的私密性和完整性,应该设置 <forms> 元素的 protection 属性,如下所示:
<forms protection="All" Privacy and integrity
限制 cookie 的生存期
限制 cookie 的生存期能够减小攻击者能够使用已捕获的 cookie 获取对您的应用程序欺骗访问权限的时间窗口。
<forms timeout="10" Reduced cookie lifetime (10 minutes)
考虑使用固定的到期时间
考虑设置 <forms> 元素的 slidingExpiration="false" 以固定 cookie 的到期时间,而不要在每次 Web 请求之后重新设置到期时间。如果您不使用 SSL 保护 cookie ,这尤其重要。
注 这一功能存在于 .NET Framework 1.1 版中。
不要持久存储身份验证 cookie
不要持久存储身份验证 cookie,因为它们存储在用户的配置文件中,如果攻击者获得了对用户计算机的物理访问权限,就可以窃取它。当您创建 FormsAuthenticationTicket 时,可以指定非持久存储 cookie,如下所示:
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 1, // version Context.User.Identity.Name, // user name DateTime.Now, // issue time DateTime.Now.AddMinutes(15), // expires every 15 mins false, // do not persist the cookie roleStr ); // user roles
使身份验证和个性化 cookie 分离
使包含特定于用户的首选项和非敏感数据的个性化 cookie 和身份验证 cookie 分离开来。窃取个性化 cookie 可能不会带来安全威胁,而攻击者能够使用窃得的身份验证 cookie 获取对您的应用程序的访问权限。
使用唯一的 cookie 名称和路径
使用唯一的 <forms> 元素的 name 和 path 属性值。通过确保名称唯一,可以防止在同一服务器寄宿多个应用程序时可能发生的各种问题。例如,如果您不使用唯一名称,在一个应用程序中已身份验证的用户就有可能请求另一个应用程序,而不用重定向到该应用程序的登录页。
有关更多信息,请参阅 Microsoft 知识库文章 313116,“ PRB:Forms Authentication Requests Are Not Directed to loginUrl Page”,和 310415,“PRB:Mobile Forms Authentication and Different Web Applications”。
使用绝对 URL 进行导航
在您的站点公开和受限区域之间(即在 HTTP 和 HTTPS 页之间)导航是一个问题,因为重定向总要使用当前页而非目标页的协议(HTTPS 或者 HTTP)。
在用户登录和浏览用 SSL 保护的目录中的页之后,相关链接例如“../publicpage.aspx”或者重定向到 HTTP 页,都将导致页使用 https 协议获得服务,这样会产生不必要的性能开销。要避免这种现象,当从一个 HTTPS 页重定向到一个 HTTP 页时,使用绝对链接例如“http://servername/appname/publicpage.aspx”。
类似地,当您从站点的公开区域重定向到安全的页(例如,登录页)时,必须使用绝对 HTTPS 路径,例如“https://servername/appname/secure/login.aspx”,而不是相对路径,例如 restricted/login.aspx。例如,如果您的 Web 页提供登录按钮,则使用以下代码重定向到安全的登录页。
private void btnLogon_Click( object sender, System.EventArgs e ) { // Form an absolute path using the server name and v-dir name string serverName = HttpUtility.UrlEncode(Request.ServerVariables["SERVER_NAME"]); string vdirName = Request.ApplicationPath; Response.Redirect("https://" + serverName + vdirName + "/Restricted/Login.aspx"); }
使用安全的凭据管理
标识欺骗是应用程序最常见的与身份验证相关的威胁之一。在攻击者伪装成另一个用户获取了对应用程序的访问权限时就会发生标识欺骗。实现的方法之一是劫持会话 cookie,但是如果您已经如前所述保护了身份验证 cookie,风险将显著减少。此外,必须构建安全的凭据管理和安全的用户存储区,以减少通过强力密码攻击、字典攻击和 SQL 注入攻击带来的风险。
以下推荐实践有助于减少风险:
-
对密码使用单向散列。
-
使用坚固的密码。
-
防止 SQL 注入。
对密码使用单向散列
如果您的用户存储区是 SQL Server,应该存储带有附加随机 salt 值的单向密码摘要(散列值)。附加的 salt 值能够减少强力密码破解企图(例如字典攻击)的风险。而摘要方法则意味着您永远都无需真正地存储密码。相反从用户那里检索密码,然后通过重新计算摘要并将其与所存储的值比较,对其进行验证。
使用坚固的密码
使用正则表达式确保用户密码遵守坚固密码的准则。以下正则表达式可以用于确保密码长度位于 8 个和 10 个字符之间,而且包含大写字母、小写字母、数字和特殊字符的组合。这将进一步减少字典攻击的风险。
private bool IsStrongPassword( string password ) { return Regex.IsMatch(password, @"^(?=.*/d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$"); }
防止 SQL 注入
窗体身份验证尤其容易出现会导致 SQL 注入攻击的漏洞,因为它使用用户提供的登录凭据查询数据库。要减少这种风险:
-
彻底验证所提供的凭据。使用正则表达式确保凭据中不包括 SQL 字符。
-
使用参数化存储过程访问用户存储区数据库。
-
使用受限和最低特权的数据库登录。
有关防止 SQL 注入的更多信息,请参阅“构建安全的数据访问”单元。
授权
您可以使用授权以控制对目录、单独的 Web 页、页类和方法的访问。如果必要,您还可以在方法代码中包括授权逻辑。在 Web 页和控件中构建授权时,考虑以下推荐实践:
-
对页和目录访问控件使用 URL 授权。
-
对 Windows身份验证使用文件授权。
-
在类和方法上使用主体要求。
-
使用显式角色检查进行细致的授权。
对页和目录访问控件使用 URL 授权
对于页级和目录级的访问控制,使用 URL 授权,这是通过 <authorization> 元素配置的。要限制对特定文件或者目录的访问,将 <authorization> 元素置于 <location> 元素中。
有关更多信息,请参阅“保护 ASP.NET 应用程序的安全”单元中的“授权”部分。
对 Windows身份验证使用文件授权
如果 ASP.NET 为 Windows身份验证进行了配置,FileAuthorizationModule 将为 ASP.NET 文件类型检查所有请求。这包括 ASP.NET 页文件 (.aspx)、用户控件 (.ascx) 和通过 IIS 映射到 ASP.NET ISAPI 筛选器的任何其他文件类型。
要配置 FileAuthorizationModule,应该在 ASP.NET 文件上设置合适的 Windows 访问控制列表 (ACL)。
在类和方法上使用主体要求
主体权限要求使您能够根据标识和调用方的角色成员资格进行授权决策。调用方的标识和角色成员资格是通过与当前 Web 请求(通过 HttpContext.User 访问)相关的主体对象维护的。可以使用声明性安全属性提供对类和方法的访问控制,如下所示:
// Declarative syntax [PrincipalPermission(SecurityAction.Demand, Role=@"DomainName/WindowsGroup")] public void SomeRestrictedMethod() { }
使用显式角色检查进行细致的授权
声明性安全检查能够防止用户访问类或者调用特定方法。如果您需要在方法中有更多逻辑进行授权决策,可以使用命令性主体权限要求或者使用 IPrincipal.IsInRole 的显式角色检查。这些方法使您能够使用更多运行时变量精细调整授权决策。以下示例说明了如何使用命令性主体权限要求:
// Imperative syntax public void SomeRestrictedMethod() { // Only callers that are members of the specified Windows group // are allowed access PrincipalPermission permCheck = new PrincipalPermission( null, @"DomainName/WindowsGroup"); permCheck.Demand(); // Some restricted operations (omitted) }
以下示例说明了如何使用 IPrincipal.IsInRole:
public void TransferMoney( string fromAccount, string toAccount, double amount) { // Extract the authenticated user from the current HTTP context. // The User variable is equivalent to HttpContext.Current.User if you // are using an .aspx page (or .asmx) WindowsPrincipal authenticatedUser = User as WindowsPrincipal; if (null != authenticatedUser) { // Note: To retrieve the authenticated user's username, use the // following line of code // string username = authenticatedUser.Identity.Name; // If the amount exceeds a threshold value, manager approval is required if (amount > thresholdValue) { // Perform a role check if (authenticatedUser.IsInRole(@"DomainName/Manager") ) { // OK to proceed with transfer } else { throw new Exception("Unauthorized funds transfer"); } } else { . . . } } }
可能还有方法允许调用方来自几种不同角色。但是,可能需要继而调用不同方法,这是不可能通过声明性安全实现的。
模拟
默认时,ASP.NET 应用程序通常出于设计、实现和可伸缩性的原因,并不模拟原始调用方。例如,模拟会防止使用有效的中间层连接池,这会对应用程序可伸缩性产生严重影响。
在一些场景中,可能要求模拟(例如,如果您要求采用替代标识(非进程标识)进行资源访问)。在寄宿环境中,经常使用多个匿名标识作为应用程序隔离的一种形式。例如,如果您的应用程序使用窗体或者 Passport 身份验证,可以模拟通过 IIS 与应用程序的虚拟目录相关的匿名 Internet 用户帐户。
您可以模拟原始调用方,它可能是匿名 Internet 用户帐户或者固定标识。要模拟原始调用方(IIS 已身份验证的标识),使用以下配置:
<identity impersonate="true" />
要模拟固定标识,使用 <identity> 元素的附加 userName 和 password 属性,但是一定要使用 Aspnet_setreg.exe 在注册表中存储加密凭据。有关在配置文件中加密凭据和 Aspnet_setreg.exe 的更多信息,请参阅“保护 ASP.NET 应用程序的安全”单元。
使用编程模拟
如果您不想对整个请求模拟一个帐户,可以使用编程模拟来模拟部分请求。例如,需要使用 ASP.NET 进程帐户访问应用程序的主要资源以及下游数据库,但是还需要访问另一个资源(例如另一个远程数据库或者远程文件共享),需要使用另一个标识。
要实现这一点,使用 IIS 将匿名用户帐户配置为可信的替代标识。然后使用以下代码只在执行远程资源访问代码时使用匿名帐户创建模拟标记:
HttpContext context = HttpContext.Current; // Get the service provider from the context IServiceProvider iServiceProvider = context as IServiceProvider; //Get a Type which represents an HttpContext Type httpWorkerRequestType = typeof(HttpWorkerRequest); // Get the HttpWorkerRequest service from the service provider // NOTE: When trying to get a HttpWorkerRequest type from the HttpContext // unmanaged code permission is demanded. HttpWorkerRequest httpWorkerRequest = iServiceProvider.GetService(httpWorkerRequestType) as HttpWorkerRequest; // Get the token passed by IIS IntPtr ptrUserToken = httpWorkerRequest.GetUserToken(); // Create a WindowsIdentity from the token WindowsIdentity winIdentity = new WindowsIdentity(ptrUserToken); // Impersonate the user Response.Write("Before impersonation: " + WindowsIdentity.GetCurrent().Name + ""); WindowsImpersonationContext impContext = winIdentity.Impersonate(); Response.Write("Impersonating: " + WindowsIdentity.GetCurrent().Name + ""); // Place resource access code here // Stop impersonating impContext.Undo(); Response.Write( "After Impersonating: " + WindowsIdentity.GetCurrent().Name + "");
注 这种方式将使用窗体或者 Passport 身份验证,这里您的应用程序的虚拟目录在 IIS 中配置为支持匿名访问。
如果您使用这个代码,需要使用以下 <identity> 配置:
<identity impersonate="false" />
注 代码要求非托管代码权限 SecurityPermission(SecurityPermissionFlag.UnmanagedCode),这种权限只授予完全可信的 Web 应用程序。
敏感数据
敏感数据包括应用程序的配置详细信息(例如,连接字符串和服务帐户凭据)和特定于应用程序的数据(例如,用户的信用卡号码)。以下推荐实践有助于减少处理敏感数据时的风险:
-
不要从页到页传递敏感数据。
-
避免在配置文件中出现明文密码。
-
使用 DPAPI 避免进行密钥管理。
-
关闭敏感数据的输出缓存。
不要从页到页传递敏感数据
应该避免使用任何客户端状态管理选项(例如查看状态、cookie、查询字符串或者隐藏窗体字段变量)来存储敏感数据。数据可能被篡改和以明文查看。使用服务器端状态管理选项(例如 SQL Server 数据库)进行安全的数据交换。
避免在配置文件中出现明文密码
在 Machine.config 和 Web.config 中的 <processModel>、<sessionState> 和 <identity> 元素有 userName 和 password 属性。不要以明文存储这些信息。在注册表中使用 Aspnet_setreg.exe 工具存储加密凭据。
有关配置文件中加密凭据和 Aspnet_setreg.exe 的更多信息,请参阅“保护 ASP.NET 应用程序和 Web 服务的安全”单元。
使用 DPAPI 避免进行密钥管理
DPAPI 用于加密机密(例如连接字符串和服务帐户凭据)是非常理想的。如果您的页需要使用这种类型的配置数据,使用 DPAPI,可以避免处理密钥管理问题。
有关更多信息,请参阅“构建安全的程序集”单元中的“加密技术”。
关闭敏感数据的输出缓存
如果您的页包含敏感的数据,例如密码、信用卡号码或者帐户状态,则不应该缓存页。要关闭对特定页的缓存,使用以下页级属性:
<%@ Page OutputCache Duration="0" Location="None" VaryByParam="None" %>
会话管理
要提供安全的会话管理,有两个主要因素需要考虑。首先,确保会话标记无法被用于获取对执行安全操作的敏感页的访问权限,或者用于获取对敏感数据项的访问权限。其次,如果会话数据包含敏感的项,必须保护会话数据,包括会话存储区。
以下两个类型的标记是与会话管理相关的:
-
会话标记。这个标记是 ASP.NET 自动生成的,如果启用会话状态,例如,通过将 <sessionState> 元素的 mode 属性设置为 InProc、SQLServer 或者 StateServer。
注 您可以改写 <sessionState> 配置并使用 @Page 标记的 EnableSessionState 属性逐页禁用或者启用会话状态。
-
身份验证标记。这是由身份验证机制例如窗体身份验证生成的,目的是跟踪已身份验证的用户的会话。有了有效的身份验证标记,用户就能够获得对 Web 站点受限部分的访问权限。
以下推荐实践有助于构建安全的会话管理:
-
要求对敏感页进行身份验证。
-
不要依赖客户端状态管理选项。
-
不要混合会话标记和身份验证标记。
-
有效地使用 SSL。
-
保护会话数据。
要求对敏感页进行身份验证
一定要在允许用户访问站点的敏感和受限部分之前对其进行身份验证。如果您使用安全的身份验证并用 SSL 保护身份验证标记,则用户的会话将是安全的,因为攻击者无法劫持和重放会话标记。攻击者将需要身份验证标记以通过授权检查。
有关如何为窗体身份验证保护身份验证标记的更多信息,请参阅本单元前面的“窗体身份验证”。
不要依赖客户端状态管理选项
不要使用任何客户端状态管理选项,例如查看状态、cookie、查询字符串或者隐藏窗体字段,来存储敏感数据。这样信息可能被篡改或者以明文查看。使用服务器端状态管理选项(例如数据库)来存储敏感数据。
不要混合会话标记和身份验证标记
安全的会话管理要求不能混合两种类型的标记。首先,保护身份验证标记,以确保攻击者无法捕获它并使用它获取访问应用程序受限区域的权限。其次,这样构建您的应用程序,使得单独使用会话标记无法用于获取对敏感页或者数据的访问权限。会话标记应该仅用于个性化目的或者在多个 HTTP 请求中维护用户状态。如果没有身份验证,就不要维护用户状态的敏感项。
有效地使用 SSL
如果您的站点既有安全区域又有公开访问区域,必须用 SSL 保护安全的身份验证区域。当用户在安全区域和公开区域来回移动的时候,ASP.NET 生成的会话 cookie(或者 URL,如果您启用了无 cookie 会话状态)将以明文形式随之移动,但是只要设置了 Secure cookie 属性,身份验证 cookie 则不会通过未加密的 HTTP 连接传递。
注 您可以通过设置 <forms> 元素的 requireSSL="true",为窗体身份验证 cookie 设置 Secure 属性。
攻击者能够获取通过未加密的 HTTP 会话传递的会话 cookie,但是如果正确设计了站点,将受限页和资源置于不同的安全目录中,则攻击者只能使用它访问不安全的公开访问页。在这种情况下,并没有什么安全威胁,因为这些页不执行敏感的操作。一旦攻击者试图重放会话标记到受保护页,因为没有身份验证标记,攻击者将被重定向到应用程序的登录页。
有关使用 Secure cookie 属性和如何构建安全的窗体身份验证解决方案的更多信息,请参阅本单元前面的“窗体身份验证”。
保护会话数据的安全
如果服务器上的会话数据包含敏感项,则数据和存储区都需要保护起来。ASP.NET 支持几种会话状态模式。有关如何保护 ASP.NET 会话状态的信息,请参阅“保护 ASP.NET 应用程序的安全”单元中的“会话状态”。
参数操作
参数,例如窗体字段、查询字符串、查看状态和 cookie 中的参数,可能被攻击者(他们通常会设法获取访问受限页的权限或者欺骗应用程序执行未授权的操作)所操作。
例如,如果攻击者知道您使用了脆弱身份验证标记方案,例如 cookie 中可以猜出的数字,攻击者就能够用另一个数字构造 cookie,并以另一个(可能是有特权的)用户身份发出请求。
以下推荐实践有助于避免参数操作漏洞:
-
用 MAC 保护查看状态。
-
使用 Page.ViewStateUserKey 对抗单击攻击。
-
在服务器上保留敏感数据。
-
验证输入参数。
用 MAC 保护查看状态
如果您的 Web 页或者控件使用查看状态在多个 HTTP 请求中维护状态,确保使用 MAC 对查看状态进行了加密和完整性检查。默认时,Machine.config 中 <pages> 元素的 enableViewStateMac 属性能够确保查看状态用 MAC 保护。
<pages buffer="true" enableSessionState="true" enableViewState="true" enableViewStateMac="true" autoEventWireup="true" validateRequest="true"/>
注 @Page 指令也支持前面的属性,这使您能够对每个页自定义设置。
虽然您可以改写是否对每个控件、页或者应用程序启用查看状态,但是只要使用查看状态,一定要将 enableViewStateMac 设置为 true。
Server.Transfer
如果您的应用程序如下所示使用 Server.Transfer,并设置可选的第二个 Boolean 参数为 true,从而使 QueryString 和 Form 集合被保护起来,则命令将失败(如果 enableViewStateMac 设置为 true)。
Server.Transfer("page2.aspx", true);
如果您忽略了第二个参数或者将其设置为 false,则错误将不会发生。如果您需要保护 QueryString 和 Form 集合而不是设置 enableViewStateMac 为 false,请遵循 Microsoft 知识库文章 316920,“PRB:View State Is Invalid" Error Message When You Use Server.Transfer”中讨论的解决方法。
有关为查看状态加密和完整性检查配置 <machineKey> 元素的信息,请参阅“保护 ASP.NET 应用程序的安全”单元。
使用 Page.ViewStateUserKey 对抗单击攻击
如果您对调用方进行身份验证,并使用查看状态,则在 Page_Init 事件处理程序中设置 Page.ViewStateUserKey 属性,以防止单击攻击。在攻击者用查看状态创建了预填充的 Web 页(.htm 或者 .aspx)时将会发生单击攻击。查看状态可以从攻击者已经创建的页(例如带有 100 项的购物车页)生成。攻击者会引诱没有产生怀疑的用户浏览到该页,然后使页发送到服务器,其中查看状态是有效的。服务器没有办法知道查看状态是否由攻击者发起。查看状态验证和 MAC 无法对抗这种攻击,因为查看状态是有效的,而页是在用户的安全上下文下执行的。
适当将 Page.ViewStateUserKey 属性设置为唯一值,可以作为对付单击攻击的对策。此值应该对于每个用户都是唯一的,而且通常是用户名称或者标识符。当攻击者创建查看状态时,ViewStateUserKey 属性将被初始化为他或者她的名称。当用户提交页给服务器时,它是用攻击者的名称初始化的。因此,查看状态 MAC 检查将失败,并生成一个异常情形。
注 这种攻击通常对于匿名浏览的页(其中没有用户名称)不成问题,因为这种类型的页应该不会进行敏感的事务。
在服务器上保留敏感数据
不要信任输入参数,尤其是它们要用于在服务器上进行安全决策的时候。同样,任何形式的敏感数据都不要使用明文参数。相反,将服务器上的敏感数据存储在会话存储区中,使用会话标记引用存储区中的项。确保用户已经安全地进行了身份验证,而且正确保护了身份验证标记。有关更多信息,请参阅本单元前面的“会话管理”。
验证输入参数
验证来自窗体字段、查询字符串、cookie 和 HTTP 头的所有输入参数。System.Text.RegularExpressions.Regex 类可以辅助对输入参数的验证。例如,以下代码说明了如何使用这个类验证通过查询字符串参数传递的名称。同一技术可以用于验证其他形式的输入参数,例如来自 cookie 或者窗体字段的参数。例如,要验证 cookie 参数,可以使用 Request.Cookies 代替 Request.QueryString。
using System.Text.RegularExpressions; . . . private void Page_Load(object sender, System.EventArgs e) { // Name must contain between 1 and 40 alphanumeric characters // together with (optionally) special characters '`´ for names such // as D'Angelo if (!Regex.IsMatch(Request.QueryString["name"], @"^[/p{L}/p{Zs}/p{Lu}/p{Ll}]{1,40}$")) throw new Exception("Invalid name parameter"); // Use individual regular expressions to validate all other // query string parameters . . . }
有关使用正则表达式和如何验证输入数据的更多信息,请参阅本单元前面的“输入验证”。
异常管理
Web 页的正确异常处理能够防止敏感的异常详细信息暴露给用户。以下推荐实践适用于 ASP.NET Web 页和控件。
-
返回一般性错误页给客户端。
-
实现页级或者应用程序级错误处理程序。
有关异常管理的更多信息,请参阅单元“构建安全的程序集”。
返回一般性错误页到客户端
如果出现未处理的异常,也就是要传播到应用程序边界的异常,应该返回一般性错误页给用户。要实现这一点,配置 <customErrors> 元素,如下所示:
<customErrors mode="On" defaultRedirect="YourErrorPage.htm" />
错误页应该包括一个合适的一般性错误消息,可能有附加的一些支持详细信息。生成错误的页名称通过 aspxerrorpath 查询参数传递给错误页。
您还可以为不同类型的错误使用多个错误页。例如:
<customErrors mode="On" defaultRedirect="YourErrorPage.htm"> <error statusCode="404" redirect="YourNotFoundPage.htm"/> <error statusCode="500" redirect="YourInternalErrorPage.htm"/> </customErrors>
对于单独的页,您可以使用以下页级属性提供一个错误页:
<% @ Page ErrorPage="YourErrorPage" %>
实现页级或者应用程序级错误处理程序
如果您需要在页级捕获和处理未处理的异常,为 Page_Error 事件创建处理程序,类似于以下代码所示。
public void Page_Error(object sender,EventArgs e) { // Get the source exception details Exception ex = Server.GetLastError(); // Write the details to the event log for diagnostics . . . // Prevent the exception from propagating and generating an // application level event (Application.Error) Server.ClearError(); }
如果异常允许从页的处理程序传播,或没有页处理程序,则将引发一个应用程序错误事件。要捕获应用程序级事件,在 Global.asax 中实现 Application_Error,如下所示:
protected void Application_Error(Object sender, EventArgs e) { // Write to the event log. }
审核和日志记录
Web 应用程序的默认 ASP.NET 进程标识能够将新的记录写到事件日志,但是它并没有足够的权限创建新的事件源。要解决这个问题,有两种选择。您可以创建一个安装程序类,它在安装时有管理员特权的情况下调用,或者您也可以在 EventLog 注册表项上配置权限,允许 ASP.NET 进程标识(或者模拟的标识)在运行时创建事件源。 推荐的是前一种方法。
要在安装时创建一个应用程序事件源
-
在 Visual Studio .NET 解决方案资源管理器窗口中右击项目,指向 Add,然后单击 Add Component。
-
从模板列表中选择 Installer Class,并提供合适的类文件名。
这将创建新的安装程序类,用 RunInstaller(true) 属性批注。
RunInstaller(true) public class EventSourceInstaller : System.Configuration.Install.Installer { . . . }
-
以设计视图显示新的安装程序类,显示工具箱,然后单击工具箱中的 Components。将一个 EventLogInstaller 组件拖动到设计器工作面。
注 如果 EventLogInstaller 没有出现在工具箱中,则右击工具箱,然后单击 Add/Remove Items。然后选择 EventLogInstaller,添加此组件类型。
-
设置以下 EventLogInstaller 属性:
-
Log。将此属性设置为 "Application",这是应该使用的事件日志的名称。您可以使用默认应用程序日志或者创建一个特定于应用程序的日志。
-
Source。将这个属性设置为事件源名称。这是通常情况下应用程序的名称。
-
-
构建项目,然后在安装时创建一个安装程序类的实例。
如果您使用 .NET 安装和部署项目创建 Windows 安装程序文件 (.msi),安装程序类实例是自动创建和调用的。如果您使用 xcopy 或者等效的部署,使用 InstallUtil.exe 实用工具创建安装程序类的一个实例并执行它。
-
要确认成功生成了事件源,可以使用注册表编辑器,导航到:
HKLM/System/CurrentControlSet/Services/EventLog/Application/{source name}
确认存在密钥,而且它包含一个指向默认 .NET Framework 事件消息文件的 EventMessageFile 字符串值:
/Windows/Microsoft.NET/Framework/{version}/EventLogMessages.dll
如果您已经有一个应用程序,不想创建安装程序类,则必须授予 ASP.NET 进程标识正确的对事件日志注册表项的访问权限。有关注册表项详细信息和精确的必需访问权限的信息,请参阅“保护 ASP.NET 应用程序的安全”单元中的“事件日志”。
EventLogPermission
写事件日志的代码必须通过代码访问安全策略被授予 EventLogPermission。如果您的 Web 应用程序配置为在部分信任级运行,这将出现问题。有关如何从部分信任 Web 应用程序写事件日志的信息,请参阅“在 ASP.NET 中使用代码访问安全”单元。
小结
本单元的开始举出了构建 Web 页和控件时需要解决的主要威胁。许多应用程序级攻击都依赖输入验证中的漏洞。在这个区域需要特别小心,以确保您的验证策略的合理性,而且对所有来自非可信来源的数据都进行了正确的验证。另一个常见的漏洞是未能保护身份验证 cookie。本单元的“窗体身份验证”部分说明了可以用来防止未授权访问、会话劫持和 cookie 重放攻击的有效对策。