构建安全的 ASP.NET 网页和控件
构建安全的 ASP.NET 网页和控件
本页内容
本模块内容 | |
目标 | |
适用范围 | |
如何使用本模块 | |
威胁和对策 | |
设计注意事项 | |
输入验证 | |
跨站点脚本 | |
身份验证 | |
授权 | |
模拟 | |
敏感数据 | |
会话管理 | |
参数处理 | |
异常管理 | |
审核和日志记录 | |
小结 | |
其他资源 |
本模块内容
Web 页和控件位于应用程序的防御前线,它们很容易受到蓄意破坏应用程序安全的攻击者的猛烈攻击。攻击者的最终目标一般是后端系统和数据存储。
像代码注入或跨站点脚本 (XSS) 这些成功的应用程序攻击都利用了服务器端应用程序漏洞。这些漏洞可造成破坏性的影响,并导致信息曝光、身份哄骗、权限提升及远程执行代码。要构建安全的 Web 页和控件,您必须遵照本模块讨论的正确编程方法。
模块首先列出并说明了常见的 ASP.NET 页和控件威胁及其相关对策,然后提供了一份必须解决的应用程序安全问题综合列表。表中包括了输入验证、输出编码、身份验证、授权、模拟、敏感数据保护、安全会话管理、参数处理保护和异常管理。这些技术都是深层安全解决方案的基本组成部分。但由于上述威胁常被忽视,无论基础结构如何安全,攻击者都能成功破坏您的系统。
目标
使用本模块可以实现:
• |
设计安全的 ASP.NET 页和控件。 |
• |
使用正则表达式和其他技术开发安全的验证代码。 |
• |
防止跨站点脚本 (XSS)。 |
• |
验证用户身份并授权。 |
• |
开发安全的表单身份验证。 |
• |
防止大量异常细节信息送达客户端。 |
• |
管理和保护 ASP.NET 会话。 |
• |
禁止处理参数。 |
• |
了解哪些对策适用于解决常见的威胁,包括代码注入、会话劫持、身份哄骗、参数处理、网络窃听、信息曝光、跨站点脚本 (XSS) 和 cookie 重播攻击。 |
适用范围
本模块适用于下列产品和技术:
• |
Microsoft® Windows® 2000 Server 和 Microsoft Windows Server™ 2003 |
• |
Microsoft .NET Framework 1.1 和 ASP.NET 1.1 |
如何使用本模块
除了后面介绍的安全编程方法之外,您还可以使用本指南的相应模块来辅助构建安全的 ASP.NET 网页和控件。
• |
执行模块 19 确保 ASP.NET 应用程序和 Web Services 的安全中的步骤。该模块可帮助您使用 Machine.config 和 Web.config 中的安全设置配置 ASP.NET。 |
• |
使用本指南检查表部分附带的检查表。检查表:保护 ASP.NET 的安全将本模块提供的建议与模块 19 提供的建议结合在一起。请确保实施这里的指南。 |
• |
了解特定于 ASP.NET 页和控件的威胁和攻击。根据本模块的指导采取相应对策。 |
• |
阅读模块 4 Web 应用程序安全设计指南。本模块(模块 10)的很多建议都基于模块 4 讨论的设计指导原则。 |
• |
设计人员应使用本模块的设计注意事项部分。 |
• |
开发人员应将本模块中的指导原则应用于开发过程。开发人员必须特别注意对输入数据的验证工作,如果该环节有安全漏洞,大部分高端应用程序级攻击便有机可乘。 |
• |
请从编程角度了解控件,以便进一步控制 ASP.NET 页和控件的安全性。 |
• |
使用应用程序安全漏洞类别来处理常见的问题。应用程序安全漏洞类别是处理和分组问题的一种有效方法。 |
威胁和对策
大多数 Web 应用程序攻击都要在 HTTP 请求中传递恶意输入项。一般,这种攻击并非强迫应用程序执行未经授权的操作,而是要中断应用程序的正常操作。因此,全面验证输入内容是解决很多攻击的必要步骤。而且,在您开发 ASP.NET Web 页和控件时,必须赋予输入验证最高的优先级。常见威胁包括:
• |
代码注入 |
• |
会话劫持 |
• |
身份哄骗 |
• |
参数处理 |
• |
网络窃听 |
• |
信息泄漏 |
图 10.1 着重介绍了 Web 应用程序面临的常见威胁。
图 10.1
“ASP.NET Web 页和控件的常见威胁”
代码注入
如果攻击者使用您应用程序的安全上下文运行任意代码,则产生代码注入攻击。如果应用程序使用特权帐户运行,将增加系统受攻击的风险。
攻击
代码注入攻击有很多类型,具体包括:
• |
跨站点脚本:将恶意脚本作为输入项发送到 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 值结合在一起保存,以降低字典攻击的风险。 |
有关在数据库中保存密码哈希值和其他机密的详细信息,请参阅模块 14 构建安全的数据访问。
参数处理
参数是网络中从客户端传递至服务器的一些数据。参数包括表单域、查询字符串、视图状态、cookie 和 HTTP 头。如果使用未经保护的参数传递敏感数据(或传递用于制定服务器安全决策的数据),应用程序可能有信息泄漏的危险,或遭受未授权的访问。
安全漏洞
可导致参数处理的安全漏洞包括:
• |
使用含敏感数据的隐藏表单域或查询字符串 |
• |
通过未经加密的连接传递含安全敏感数据的 cookie |
攻击
参数处理攻击包括:
• |
Cookie 重播攻击:攻击者捕获并改变了 cookie,然后在应用程序中重播。如果 cookie 中包含的数据用于在服务器中进行身份验证或授权,很容易导致身份哄骗和权限提升。 |
• |
处理隐藏表单域:这些域包含了在服务器中制定安全决策要用的数据。 |
• |
处理查询字符串参数 |
对策
可采取下列对策防止参数处理:
• |
不依赖于客户端状态管理选项。避免使用任何客户端状态管理选项保存敏感数据。这些选项包括:视图状态、cookie、查询字符串或隐藏表单域。 |
• |
在服务器中保存敏感数据。使用会话令牌将用户会话与服务器中维护的敏感数据项相关联。 |
• |
使用消息身份验证代码 (MAC) 保护会话令牌。将令牌与服务器中相应的身份验证、授权和业务逻辑相匹配,确保令牌不处于重播状态。 |
网络窃听
网络窃听涉及使用网络监视软件跟踪传递于浏览器和 Web 服务器之间的数据包。它可导致泄漏应用程序特定的机密数据、检索登录凭据或捕获身份验证 cookie。
安全漏洞
可导致网络窃听攻击成功的安全漏洞包括:
• |
发送敏感数据时未加密 |
• |
通过未经加密的通道发送身份验证 cookie |
攻击
使用网络中的数据包嗅探 (sniff) 工具可实施网络窃听攻击,从而捕获网络数据流。
对策
要对抗网络窃听,请使用安全套接字层 (SSL) 在浏览器和 Web 服务器间建立加密通信通道。只要在网络中发送凭据、身份验证票证或敏感的应用程序数据,就一定要使用 SSL。
信息泄漏
如果攻击者通过探测 Web 页来找寻引起异常的各种情况,则出现消息泄漏攻击。对于攻击者而言,这是一种颇有成效的攻击方法。因为异常细节信息常以 HTML 的形式返回并显示在浏览器中。这可能会泄漏很有用的信息,如堆栈跟踪。堆栈跟踪包含数据库连接字符串、数据库名、数据库方案信息、SQL 语句以及操作系统和平台版本。
安全漏洞
可导致信息泄漏的安全漏洞包括:
• |
异常处理能力薄弱 |
• |
原始的例外细节信息被传播至客户端 |
攻击
可导致信息泄漏的攻击有多种,具体包括:
• |
缓冲区溢出。 |
• |
有意发送格式错误的输入。 |
对策
可采取下列措施防止信息泄漏:
• |
使用结构化异常处理。 |
• |
将一般错误页返回客户端。 |
• |
使用包含一般错误和无害错误消息的默认重定向页。 |
设计注意事项
开发 Web 页和控件之前,必须考虑很多重要问题。关键的注意事项如下:
• |
使用服务器端输入验证。 |
• |
对 Web 站点进行分区。 |
• |
考虑访问资源的身份。 |
• |
保护凭据和身份验证票证。 |
• |
安全地失败。 |
• |
考虑授权粒度。 |
• |
将 Web 控件和用户控件置于独立的程序集中。 |
• |
将资源访问代码置于独立的程序集中。 |
使用服务器端输入验证
在设计时,请明确 Web 页和控件所处理的全部不同用户输入源。包括表单域、查询字符串、从 Web 用户处收到的 cookie 以及来自后端数据源的数据。很显然,Web 用户并不属于应用程序信任范畴,因此,您必须在服务器中验证来自该数据源的所有输入。除非绝对信任从后端数据源中检索的数据,否则请务必在将数据发送到客户端之前进行验证和净化。确保解决方案不依赖于客户端验证,因为客户端验证很容易被忽视。
对 Web 站点进行分区
Web 站点的设计必须区分公共访问部分和要求身份验证的受限部分。使用应用程序虚拟根目录下独立的子目录可维护各种受限的页面。例如,典型电子商务网站中的结帐功能要求验证访问者的身份来传递信用卡号等敏感数据。独立的子目录允许您应用附加的安全性(例如,通过要求 SSL),但不会给整个站点带来 SSL 性能负荷此外,它还允许您限制 HTTPS 连接中的身份验证 cookie 传递来缓解会话劫持风险。图 10.2 显示了一种典型的分区。
图 10.2
划分为公共区域和安全区域的 Web 站点
请注意,在图 10.2 中,受限的子文件夹使用 Internet 信息服务 (IIS) 配置,要求通过 SSL 访问。Web.config 中的第一个 <authorization> 元素允许所有用户访问公共区域,第二个元素禁止未经授权的用户访问安全子文件夹的内容并强制登录。
有关限制身份验证 cookie,使其只能通过 HTTPS 连接传递的详细信息,以及如何在受限和非受限页面间导航的详细信息,请参阅本模块身份验证部分的“使用绝对 URL 导航”。
考虑访问资源的身份
在默认情况下,ASP.NET 应用程序不模拟帐户,而是使用特权最低的 ASPNET 进程帐户运行 ASP.NET Web 应用程序来访问资源。默认值是使用推荐的配置。在很多情况下,您可能需要使用不同的 Windows 安全上下文访问资源,具体情况包括:
• |
在同一服务器中驻留多个应用程序 |
• |
访问有特定身份验证要求的远程资源 |
保护凭据和身份验证票证
您的设计必须考虑保护凭据和身份验证票证安全的方面。如果凭据是通过网络传递的,且位于永久存储(如配置文件)中,您必须保护这些凭据。由于身份验证票证容易遭受劫持攻击,必须确保票证在网络中的安全。加密即是一种解决方案。此外,您还可以使用 SSL 或 IPSec 来保护网络中的凭据和身份验证票证。DPAPI 是一种在配置文件中加密凭据的很好的解决方案。
安全地失败
如果应用程序出现难以恢复的异常情况,请确保应用程序安全地失败,不要造成系统完全对外部敞开的局面。防止对恶意用户而言重要的异常细节信息被传播给客户端,确保仅返回一般错误页。规划使用结构化的异常处理来处理错误,而不是依赖方法错误代码来处理错误。
考虑授权粒度
考虑您在站点身份验证中使用的授权粒度。如果目录的配置要求身份验证,是否所有的用户都有相等的权限来访问该目录中的页面?如果需要,可基于身份(或常用的调用者角色成员身份)为不同页面应用不同的授权规则。方法是,在不同的 <location> 元素中使用 <authorization> 元素。
例如,同一目录中的两个页面在 Web.config 文件中有不同的 <allow> 元素和 <deny> 元素。
将 Web 控件和用户控件置于独立的程序集中
如果将 Web 控件和用户控件置于各自的程序集中,可通过代码访问安全策略分别配置每个程序集的安全性。这为管理员提供了更多的灵活性。换言之,您不必仅为满足单个控件的需要而向所有控件授予扩展权限。
将资源访问代码置于独立的程序集中
使用独立的程序集,然后根据页类(非页类事件处理程序中的嵌入资源访问代码)来调用这些程序集。这可提升代码访问安全策略的灵活性,并对构建部分信任 Web 应用程序起着特别重要的作用。有关详细信息,请参阅模块 9 ASP.NET 代码访问安全性。
输入验证
如果对输入的类型、长度、格式和范围做出毫无根据的假设,应用程序可能失败。如果攻击者发现了您做出的毫无根据的假设,输入验证可能成为安全问题。此时,攻击者可能提供构思巧妙的输入,使您的应用程序受到一定的损害。在 Web 应用程序中,误信用户输入是最常见且最具破坏性的安全漏洞之一。
约束并净化
首先是限制输入内容,然后通过验证输入的类型、长度、格式和范围检查正确数据。有时,您可能还必须净化输入,从而确保可能的恶意输入的安全。例如,如果应用程序支持任意格式的输入字段(如注释字段),您可能要使用一些“安全”HTML 元素,如 <b> 和 <i>,然后放弃其他所有 HTML 元素。下表汇总了在约束和净化数据中可用的选项:
表 10.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 类
如果在常规的 HTML 控件中不使用 runat="server" 属性(除了使用 RegularExpressionValidator 控件的情形),或需要验证来自其他源(像查询字符串或 cookie)中的输入,则可在页类或验证帮助程序方法中使用 Regex 类(可能在独立的程序集中)。本节稍后将给出一些示例。
正则表达式注释
如果使用下面的语法并使用表达式 # 对每个组件进行注释,则更容易了解正则表达式。要启用注释,还必须指定 RegexOptions.IgnorePatternWhitespace,它表明忽略非转义空格。
Regex regex = new Regex(@" ^ # 起始标记 (?=.*\d) # 必须至少包含一个数字 (?=.*[a-z]) # 必须包含一个小写字母 (?=.*[A-Z]) # 必须包含一个大写字母 .{8,10} # 字符长度从 8 到 10 $ # 结束标记, RegexOptions.IgnorePatternWhitespace);
字符串字段
要验证字符串字段(如名称、地址和纳税辨识号等),请使用正则表达式执行下列操作:
• |
约束输入字符的允许范围。 |
• |
应用格式规则。例如,基于模式的字段(如纳税辨识号、ZIP 代码或邮政编码)必须使用特定模式的输入字符。 |
• |
检查长度。 |
名称
下面的示例说明了已在验证名称字段中使用的 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}$")) { // 社会保险号无效 }
日期字段
具有相同 .NET Framework 类型的输入字段可由 .NET Framework 类型系统来检查类型。例如,要验证日期,您可以在输入数据不兼容的情况下,将输入值转换成 System.DateTime 类型的变量,然后处理得到的所有格式异常,如下所述。
try { DateTime dt = DateTime.Parse(txtDate.Text).Date; } // 如果类型转换失败,将触发 FormatException catch( FormatException ex ) { // 将无效日期消息返回给调用者 }
除了检查格式和类型,您可能还需检查日期字段的范围。使用 DateTime 可轻松执行此项操作,如下所述。
// 为简单起见省略了异常处理 DateTime dt = DateTime.Parse(txtDate.Text).Date; // 日期必须是今天或之前 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 { // 如果无效,转换将触发异常。 int i = Convert.ToInt32(sInput); if ((0 <= i && i <= 255) == true) { // 数据无效,请使用编号 } } 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) { // 请注意,IsPostBack 仅适用于 // 服务器形式(有 runat="server") if ( Request.RequestType == "POST" ) // 非服务器形式 { // 验证提供的电子邮件地址 if( !Regex.Match(Request.Form["email"], @"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$", RegexOptions.None).Success) { // 无效电子邮件地址 } // 验证提供的名称 if ( !RegEx.Match(Request.Form["name"], @"^[A-Za-z'\- ]$", RegexOptions.None).Success) { // 无效名称 } } }
验证用于访问数据的输入
如果基于用户输入来生成动态 SQL 查询,SQL 注入攻击可注入由数据库执行的恶意 SQL 命令。在典型的基于 Web 的数据访问中,可使用下列深层防御战略:
• |
使用正则表达式来约束页类中的输入。 |
• |
净化或拒绝输入。为了进行深层防御,您可以借助帮助程序去掉空字符或其他已知的“坏”字符。 |
• |
为数据访问使用参数化存储过程,确保检查 SQL 查询中使用的数据的类型和长度。 |
有关为数据访问使用参数及编写安全数据访问代码的详细信息,请参阅模块 14 构建安全的数据访问。
验证用于文件 I/O 的输入
通常,您应避免使编写的代码接受来自调用者的文件输入或路径输入。正确的做法是,在读取和编写数据时使用固定的文件名和位置。这可确保您的代码不会被用来强制访问任意文件。此外,还可确保代码不容易遭受规范化 Bug 的攻击。
如果确实要接受输入文件名,需面临两个主要挑战。首先,结果文件路径和名称是否是有效的文件系统名称?其次,路径是否在应用程序上下文中有效?例如,路径是否位于应用程序虚拟目录的根目录下?
要使文件名规范化,请使用 System.IO.Path.GetFullPath。要检查文件路径是否在应用程序上下文中有效,可使用 .NET 代码访问安全性向您的代码授予精确的 FileIOPermission,以便只能访问特定目录中的文件。有关详细信息,请参阅模块 7 构建安全的程序集中的“文件 I/O”部分和模块 8 代码访问安全的实践。
使用 MapPath
如果使用 MapPath 将提供的虚拟路径映射到服务器中的某一物理路径,请使用接受 bool 参数的 Request.MapPath 重载,以防止跨应用程序映射,如下所示:
try { string mappedPath = Request.MapPath( inputPath.Text, Request.ApplicationPath, false); } catch (HttpException) { // 尝试跨应用程序映射 }
最终的 false 参数可防止跨应用程序映射。这意味着,用户不能成功提供包含“..”的路径来遍历应用程序虚拟目录层次结构的外部。任何执行该操作的企图都将造成 HttpException 异常类型。
注意:服务器控件可使用 Control.MapPathSecure 方法来读取文件。此方法要求代码访问安全策略授予调用代码以完全信任;否则将触发 HttpException。有关详细信息,请参阅 .NET Framework SDK 文档中的 Control.MapPathSecure。
常用正则表达式
Visual Studio .NET 提供了一组非常有用的正则表达式。要访问这些表达式,请在 Web 窗体中添加 RegularExpresssionValidator 控件,然后单击控件 Expression 属性字段中的省略号按钮。下表显示了另外几个在常用 Web 页字段中非常有用的表达式。
表 10.2:常用的正则表达式字段
字段 | 表达式 | 格式示例 | 说明 |
名称 |
[a-zA-Z'`-´\s]{1,40} |
John DoeO'Dell |
验证名称。最多允许使用 40 个大写字母和小写字母,以及一些在名称中常用的特殊字符。此列表可进行调整。 |
数字 |
^\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。 |
邮政编码 |
^(\d{5}-\d{4}|\d{5}|\d{9})$|^([a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d)$ |
|
验证允许使用 5 个或 9 个数字的美国邮政编码。 |
密码 |
^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ |
|
验证强密码。字符数必须在 8 至 10 的范围内。必须包含大小写字母和数字的组合,不能使用特殊字符。 |
非负整数 |
\d+ |
0986 |
验证大于零的整数。 |
货币(非负数) |
"\d+(\.\d\d)?" |
|
验证正货币金额。要求小数点后有两位数字。 |
货币(正数或负数) |
"(-)?\d+(\.\d\d)?" |
|
验证正负货币金额。要求小数点后有两位数字。 |
跨站点脚本
XSS 攻击可通过注入客户端脚本代码来探测 Web 页验证中的漏洞。代码随后被送回信任用户,并由浏览器来执行。由于浏览器是从信任站点下载脚本代码的,因此不识别代码是否合法,Internet Explorer 安全区域不提供任何防御措施。此外,XSS 攻击还可通过 HTTP 或 HTTPS (SSL) 连接来起作用。最严重的探测情况是,攻击者编写脚本来检索提供信任站点访问权限的身份验证 cookie,然后将该 cookie 传递给攻击者已知的 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 页中有任意格式的文本框(如“注释”字段),而您希望在其中使用较安全的 HTML 元素(如 <b> 和 <i>),则可首先使用 HtmlEncode 进行预处理,然后有选择性地删除许可元素中的编码,从而实现安全处理的目的,如下所示:
StringBuilder sb = new StringBuilder( HttpUtility.HtmlEncode(userInput) ) ; sb.Replace("<b>", "<b>"); sb.Replace("</b>", "</b>"); sb.Replace("<i>", "<i>"); sb.Replace("</i>", "</I>"); Response.Write(sb.ToString());
深层防御对策
除了前面提到的技术,您还可以使用下列深层防御对策来防止 XSS:
• |
设置正确的字符编码。 |
• |
使用 ASP.NET 版本 1.1 的 validateRequest 选项。 |
• |
在 Web 服务器中安装 URLScan。 |
• |
使用 HttpOnly cookie 选项。 |
• |
使用 <frame> 安全属性。 |
• |
使用 innerText 属性。 |
设置正确的字符编码
要成功限制在 Web 页中有效的数据,最重要的是限制输入数据的表示方式。这可防止恶意用户使用规范化和多字节转义序列来哄骗输入验证例程。
借助于 Web.config 中的 <globalization> 元素,ASP.NET 允许您指定页面级或应用程序级的字符集。下面介绍了这两种方法,二者均使用了 HTML 和 HTTP 早期版本中的默认 ISO-8859-1 字符编码。
要设置页面级字符编码,请使用 <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) { // 名称的字母字符数必须介于 1 至 40 之间 // 还可在名称中使用(可选)特殊字符 '`´,例如, // D'Angelo if (!Regex.IsMatch(Request.Form["name"], @"^[\p{L}\p{Zs}\p{Lu}\p{Ll}]{1,40}$")) throw new ArgumentException("Invalid name parameter"); // 使用单独的正则表达式来验证其他参数 . . . }
下面的内容说明了上述代码中的正则表达式:
• |
{<name>} 指定一个已命名的 Unicode 字符类。 |
• |
\p{<name>} 将已命名字符类中的任意字符与 {<name>} 中指定的字符匹配。 |
• |
{L} 按从左到右的顺序匹配。 |
• |
{Lu} 执行大写字母匹配。 |
• |
{Ll} 执行小写字母匹配。 |
• |
{Zs} 匹配分隔符和空格。 |
• |
{1,40} 表示字符数大于等于 1,小于等于 40。 |
• |
{Mn} 匹配标记和无空格字符。 |
• |
{Zs} 匹配分隔符和空格。 |
• |
* 指定无匹配项或有多个匹配项。 |
• |
$ 表示停止在此位置查找。 |
使用 ASP.NET 的 validateRequest 选项
validateRequest 属性是 .NET Framework 版本 1.1 的特点之一。在默认情况下,该属性在 Machine.config 的 <pages> 元素中设置,且值为“True”。该属性要求 ASP.NET 检查从浏览器中接收的所有数据是否有潜在的恶意输入,例如包含 <script> 元素的输入。ASP.NET 可检查从 HTML 表单域、cookies 和查询字符串中接收的输入。.NET Framework 版本 1.0 不提供任何等价功能,但 IIS URLScan Internet 服务器应用程序编程接口 (ISAPI) 筛选器可执行类似工作。您还可以将该设置应用于每个使用 @ Page 标记的页面,如下所示:
<% @ Page validateRequest="True" %>
在 Web 服务器中安装 URLScan
URLScan 是一种运行 IISLockdown 工具时安装的 ISAPI 筛选器。它有助于缓解 XSS 攻击带来(通过注入潜在的恶意输入)的风险。有关 IISLockdown 和 URLScan 的详细信息,请参阅模块 16 保护 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 属性。要将 HttpOnly 属性添加到 cookie 中,您必须使用 ISAPI 筛选器;如果要使用托管代码解决方案,请将下面的代码添至应用程序的 Application_EndRequest 事件处理程序中(在 Global.asax 中):
protected void Application_EndRequest(Object sender, EventArgs e) { string authCookie = FormsAuthentication.FormsCookieName; foreach (string sCookie in Response.Cookies) { // 只要在表单身份验证 cookie 中设置 HttpOnly 属性 // 跳过该检查可在集合中的所有 cookie 中设置该属性 if (sCookie.Equals(authCookie)) { // 强制将 HttpOnly 添加到 cookie 头中 Response.Cookies[sCookie].Path += ";HttpOnly"; } } }
注意:在 .NET Framework 将来的版本中,Cookie 类可能有 HttpOnly 属性。
使用 <frame> 安全属性
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>
下列建议可帮助您构建安全的表单身份验证解决方案:
• |
对网站进行分区。 |
• |
确保在受限页面安全方面使用 SSL。 |
• |
使用 URL 授权。 |
• |
确保身份验证 cookie 安全。 |
• |
使用绝对 URL 导航。 |
• |
使用安全的凭据管理。 |
对网站进行分区
在站点设计中,确保将那些要求验证访问身份的安全页面置于不同于匿名访问页面所在的子目录中。图 10.3 显示了 Visual Studio .NET 解决方案资源管理器窗口中的典型排列。请注意,窗体登录页与其他受限页面一起放在独立的子目录中。
图 10.3
“要求验证访问身份的受限页面所在的子目录”
注意:如果在应用程序中使用 Server.Transfer 从匿名页面中提取信息并传输至安全页面,.NET Framework 版本 1.1 或早期版本将绕过身份验证检查,因此您必须验证使用 Server.Transfer 的代码,确保不会传输到安全目录中。
确保在受限页面的安全方面使用 SSL
要确保使用 SSL 来保护从登录窗体投递的登录凭据,以及传递给受限页面后续请求的身份验证 cookie,请配置 IIS 中的安全文件夹,使之使用 SSL。这将设置 IIS 元数据库中文件夹的 AccessSSL=true 属性。只有在请求 URL 中使用 https,对安全文件夹中页面的请求才能成功。
对于 SSL,您必须在 Web 服务器中安装服务器证书。有关详细信息,请参阅 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/secnetlpMSDN.asp 中的“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications: Authentication, Authorization, and Secure Communication”的“How To: Setup SSL on a Web Server”(英文)。
使用 URL 授权
要许可匿名访问公共页面,请使用下面的 <authorization> 元素。
<system.web> <!-- 虚拟目录根文件夹包含常规页面。 未经身份验证的用户可查看它们,但不需要 使用 SSL 保护。 --> <authorization> <allow users="*" /> </authorization> </system.web>
使用下面的 <authorization> 元素(位于 Web.config 中的 <location> 元素内)可拒绝未经身份验证的用户的访问,并强制重定向至 <forms> 元素指定的登录页:
<!-- 受限文件夹仅用于身份验证和 SSL 访问。 --> <location path="Secure" > <system.web> <authorization> <deny users="?" /> </authorization> </system.web> </location>
确保身份验证 cookie 的安全
要防止会话劫持和 cookie 重播,请确保 cookie 只能通过使用 HTTPS 协议的 SSL 连接,从而确保其安全。为了减少其他风险,请在将 cookie 发至客户端前对其进行加密,并限制有效期。要确保身份验证 cookie 安全,请执行下列操作:
• |
限定身份验证 cookie 只能通过 HTTPS 连接传递。 |
• |
加密 cookie。 |
• |
限制 cookie 的生命周期。 |
• |
考虑使用固定的有效期。 |
• |
不保留身份验证 cookie。 |
• |
使身份验证 cookie 和个性化 cookie 分开。 |
• |
使用独特的 cookie 名称和路径。 |
限定身份验证 cookie 只能通过 HTTPS 连接传递
cookie 支持使用“secure”属性确定浏览器是否应将 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)) { // 设置安全 cookie。浏览器只将 cookie // 发送至使用 https 请求的页面 Response.Cookies[sCookie].Secure = true; } } }
加密 cookie
即便已使用 SSL,请加密 cookie 内容。这可在攻击者企图利用 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, // 版本 Context.User.Identity.Name, // 用户名 DateTime.Now, // 发布时间 DateTime.Now.AddMinutes(15), // 每隔 15 分钟过期 false, // 不保留 cookie roleStr ); // 用户角色
使身份验证 cookie 和个性化 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 导航
由于重定向始终使用当前页(非目标页)的协议(HTTPS 或 HTTP),因此在站点公共区域和受限区域间(即在 HTTP 和 HTTPS 页间)导航会发生问题。
一旦用户登录并浏览通过 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 ) { // 使用服务器名和 v-dir 名称构建绝对路径 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 ) { }
禁止 SQL 注入
鉴于用户提供的登录凭据查询数据库的方式,表单身份验证特别容易受到 SQL 注入攻击。要降低风险,请执行下列操作:
• |
全面验证提供的凭据。使用正则表达式确保 SQL 字符不包含在内。 |
• |
使用参数化存储过程访问用户存储数据库。 |
• |
使用受限且特权最低的数据库登录。 |
有关禁止 SQL 注入的详细信息,请参阅模块 14 构建安全的数据访问。
授权
您可通过授权来控制对目录、独立 Web 页、页类和方法等的访问。如果需要,还可在方法代码中包含授权逻辑。在 Web 页和控件中构建授权时,请考虑下列建议:
• |
使用 URL 授权来控制对 Web 页和目录的访问。 |
• |
使用包含 Windows 身份验证的文件授权。 |
• |
在类和方法中使用主体请求。 |
• |
使用显式的角色检查来精确授权。 |
使用 URL 授权来控制对 Web 页和目录的访问
对于页面级和目录级的访问控制,请使用 URL 授权(通过 <authorization> 元素配置)。要限制对特定文件或目录的访问,请将 <authorization> 元素置于 <location> 元素中。
有关详细信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全中的“授权”部分。
使用包含 Windows 身份验证的文件授权
如果 ASP.NE 要求 Windows 身份验证,FileAuthorizationModule 将检查 ASP.NET 文件类型的所有请求。这些文件类型有:ASP.NET 页文件 (.aspx)、用户控件 (.ascx) 以及通过 IIS 映射到 ASP.NET ISAPI 筛选器的任意其他文件类型。
要配置 FileAuthorizationModule,请在 ASP.NET 文件中设置相应的 Windows 访问控制列表 (ACL)。
在类和方法中使用主体请求
主体权限请求 (Principal permission demand) 允许您基于调用者的身份和角色成员资格制定授权决策。调用者的身份和角色成员资格由主体对象维护。主体对象与当前 Web 请求(通过 HttpContext.User 访问)相关联。使用声明性安全属性可控制对类和方法的访问,如下所述:
// 声明语法 [PrincipalPermission(SecurityAction.Demand, Role=@"DomainName\WindowsGroup")] public void SomeRestrictedMethod() { }
使用显式角色检查制定精确授权
声明性安全检查可防止用户访问类或调用特定的方法。如果要在方法中使用附加逻辑来制定授权决策,请使用命令式主体权限请求,或通过 IPrincipal.IsInRole 使用显式角色检查。这些方法允许您使用附加的运行时变量来优化授权决策。下面的示例显示了使用命令式主体权限请求的情况:
// 命令式语法 public void SomeRestrictedMethod() { // 仅允许属于指定 Windows 组成员的调用者 // 访问 PrincipalPermission permCheck = new PrincipalPermission( null, @"DomainName\WindowsGroup"); permCheck.Demand(); // 一些受限操作(省略) }
下面的示例显示了 IPrincipal.IsInRole 的用法:
public void TransferMoney( string fromAccount, string toAccount, double amount) { // 从当前 HTTP 上下文提取经身份验证的用户。 // 用户变量等于 HttpContext.Current.User,条件是您 // 使用 .aspx 页(或 .asmx) WindowsPrincipal authenticatedUser = User as WindowsPrincipal; if (null != authenticatedUser) { // 注意:要检索经身份验证的用户的用户名,请使用 // 下列代码行 // 字符串用户名 = authenticatedUser.Identity.Name; // 如果值超过阈值,必须通过管理员的审批 if (amount > thresholdValue) { // 执行角色检查 if (authenticatedUser.IsInRole(@"DomainName\Manager") ) { // 如果确定,可继续传送 } 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 的详细信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全。
使用编程模拟
如果不想模拟整个请求的帐户,可使用编程模拟来模拟部分请求。例如,您希望使用 ASP.NET 进程帐户来访问应用程序的主要资源和下游数据库,但需要使用备用身份访问备用的资源(如另一远程数据库或远程文件共享)。
为此,请使用 IIS 配置匿名用户帐户,使其成为受信任的备用身份。然后,仅在执行远程资源访问代码时再使用匿名帐户创建模拟令牌,代码如下:
HttpContext context = HttpContext.Current; // 从上下文中获取服务提供程序 IServiceProvider iServiceProvider = context as IServiceProvider; //获取代表 HttpContext 的类型 Type httpWorkerRequestType = typeof(HttpWorkerRequest); // 从服务提供程序中获得 HttpWorkerRequest 服务 // 注意:如果尝试从 HttpContext 中获取 HttpWorkerRequest 类型, // 必须有未托管代码的权限。 HttpWorkerRequest httpWorkerRequest = iServiceProvider.GetService(httpWorkerRequestType) as HttpWorkerRequest; // 获得 IIS 传递的令牌 IntPtr ptrUserToken = httpWorkerRequest.GetUserToken(); // 根据令牌创建 WindowsIdentity WindowsIdentity winIdentity = new WindowsIdentity(ptrUserToken); // 模拟用户 Response.Write("Before impersonation: " + WindowsIdentity.GetCurrent().Name + "<br>"); WindowsImpersonationContext impContext = winIdentity.Impersonate(); Response.Write("Impersonating:" + WindowsIdentity.GetCurrent().Name + "<br>); // 在此放置资源访问代码 // 停止模拟 impContext.Undo(); Response.Write( "After Impersonating: " + WindowsIdentity.GetCurrent().Name + "<br>");
注意:对于这种方法,如果应用程序虚拟目录在 IIS 中配置为支持匿名访问,则该方法使用表单或 Passport 身份验证。
如果使用这里的代码,请使用下面的 <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 的详细信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全。
使用 DPAPI 避免密钥管理
DPAPI 非常适于加密机密信息,如连接字符串和服务帐户凭据。如果页面要使用这种类型的配置数据,请使用 DPAPI 避免密钥管理问题。
有关详细信息,请参阅模块 7 构建安全的程序集中的“加密”部分。
关闭敏感数据的输出缓冲功能
如果页面包含敏感数据(如密码、信用卡号或帐户状况),不应对该页进行缓存。要关闭特定页面的缓存功能,请使用下列页级属性:
<%@ Page OutputCache Duration="0" Location="None" VaryByParam="None" %>
会话管理
在提供安全会话管理时,您应考虑下面两大主要因素。首先,确保会话令牌无法用来获取敏感页面(其中执行安全的操作)的访问权限,或获取敏感数据项的访问权限。其次,如果会话数据中有敏感内容,必须确保会话数据(包括会话存储)的安全。
下面两种令牌类型都与会话管理关联:
• |
会话令牌:如果启用会话状态,ASP.NET 将自动生成该令牌。例如,将 <sessionState> 元素的 mode 属性设置为 InProc、SQLServer 或 StateServer。 注意:您可以使用 @Page 标记中的 EnableSessionState 属性覆盖 <sessionState> 配置,或基于页面启用(或禁用)会话状态。 |
• |
身份验证令牌:该令牌由身份验证机制(如表单身份验证)生成,用于跟踪身份经过验证的用户的会话。如果使用有效的身份验证令牌,用户可访问网站中的受限区域。 |
下列建议有助于您构建安全的会话管理:
• |
要求对敏感页面进行身份验证。 |
• |
不依赖于客户端的状态管理选项。 |
• |
不混用会话令牌和身份验证令牌。 |
• |
有效使用 SSL。 |
• |
确保会话数据安全。 |
要求对敏感页面进行身份验证
确保先验证用户的身份,然后再许可用户访问站点中的敏感区域和受限区域。如果使用安全身份验证并通过 SSL 保护身份验证令牌,用户会话就是安全的,因为攻击者无法拦截和重播会话令牌。攻击者必须使身份验证令牌通过授权网关。
有关保护表单身份验证的身份验证令牌的详细信息,请参阅本模块前面介绍的表单身份验证。
不依赖于客户端的状态管理选项
避免使用任何客户端状态管理选项(例如,视图状态、cookie、查询字符串或隐藏表单域)来保存敏感数据。这些信息可被篡改并明文查看。请使用服务器端的状态管理选项(如数据库)来保存敏感数据。
不混用会话令牌和身份验证令牌
根据安全会话管理的要求,不能混用两种类型的令牌。首先,确保身份验证令牌安全,确保攻击者无法捕获该身份验证令牌,然后使用它访问应用程序中的受限区域。其次,在构建应用程序时,确保单独的会话令牌无法用于访问敏感页面或敏感数据。会话令牌只能用于个性化用途中,或维护跨多个 HTTP 请求的用户状态。如果不进行身份验证,请不要维护用户状态中的敏感项目。
有效使用 SSL
如果站点中既有安全区域,也有公共访问区域,必须使用 SSL 保护要求身份验证的安全区域。如果用户在安全区域和公共区域间来回移动,ASP.NET – 生成的会话 cookie(如果启用了 cookie-less 会话状态,则是 URL)将随用户一起以纯文本形式移动。但是,只要设置了 Secure cookie 属性,身份验证 cookie 便永远不在未经加密的 HTTP 连接中传递。
注意:可以通过设置 <forms> 元素中的 requireSSL="true" 来设置表单身份验证 cookie 的 Secure 属性。
攻击者可获取传递于未经加密的 HTTP 会话中的会话 cookie,但如果已正确设计站点,并将受限页面和资源置于独立的安全目录中,攻击者只能使用该 cookie 访问不安全的公共访问页。此时,不存在任何安全方面的威胁,因为这些页面并不执行敏感操作。一旦攻击者在安全页面中重播会话令牌,则由于没有身份验证令牌,攻击者只能被重定向至应用程序的登录页。
有关使用 Secure cookie 属性和构建安全表单身份验证解决方案的详细信息,请参阅本模块前面的表单身份验证。
确保会话数据安全
如果服务器中的会话数据包含敏感内容,必须确保数据和存储的安全。ASP.NET 支持多种会话状态模式。有关确保 ASP.NET 会话状态的相关信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全中的“会话状态”部分。
参数处理
对于常企图访问受限页面或欺骗应用程序执行未授权操作的攻击者而言,常要处理各种参数,如来自表单域、查询字符串、视图状态和 cookie 中的参数。
例如,如果攻击者获悉,您的 cookie 使用的身份验证令牌方案比较薄弱(如号码可猜到),他(或她)就会使用另一号码构建 cookie,然后以不同的用户身份(可能是特权用户)发出请求。
下列建议有助于避免参数处理安全漏洞:
• |
使用 MAC 保护视图状态。 |
• |
使用 Page.ViewStateUserKey 对付一键式 (one-click) 攻击。 |
• |
维护服务器中的敏感数据。 |
• |
验证输入参数。 |
使用 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,并将第二个可选布尔值参数设置为 True 来保留 QueryString 和 Form 组合,则命令将在 enableViewStateMac 设置为 True 时失败。
Server.Transfer("page2.aspx", true);
如果省略第二个参数或将其设置为 false,则不会出现错误。如果要保留 QueryString 和 Form 的组合,而不是将 enableViewStateMac 设置为 false,请参考 Microsoft 知识库文章 16920 PRB:"View State Is Invalid" Error Message When You Use Server.Transfer(英文)中的解决方案。
有关配置 <machineKey> 元素来加密视图状态和执行完整性检查的详细信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全。
使用 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) { // 名称的字母字符数必须介于 1 至 40 之间 还可在名称中使用(可选)特殊字符 '`´,例如, // D'Angelo if (!Regex.IsMatch(Request.QueryString["name"], @"^[\p{L}\p{Zs}\p{Lu}\p{Ll}]{1,40}$")) throw new Exception("Invalid name parameter"); // 使用独立的正则表达式验证其他所有内容 // 查询字符串参数 . . . }
有关使用正则表达式和验证输入数据的详细信息,请参阅本模块前面的输入验证。
异常管理
Web 页中正确的异常处理可防止向用户泄漏敏感的异常细节信息。下面的建议适用于 ASP.NET Web 页和控件。
• |
将一般错误页返回给客户端。 |
• |
实施页面级或应用程序级错误处理程序。 |
有关异常管理的详细信息,请参阅模块 7 构建安全的程序集。
将一般错误页返回给客户端
如果有未处理的异常(即传播至应用程序边界的异常),一般错误页将返回给用户。为此,请按如下方式配置 <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) { // 获得源异常细节信息 Exception ex = Server.GetLastError(); // 将详细信息写入事件日志以便诊断 . . . // 防止异常传播并生成 // 应用程序级事件 (Application.Error) Server.ClearError(); }
如果允许从页处理程序传播异常,或不存在页处理程序,则出现应用程序错误事件。要捕获应用程序级事件,请实施 Global.asax 中的 Application_Error,如下所述:
protected void Application_Error(Object sender, EventArgs e) { // 写入事件日志。 }
审核和日志记录
对于 Web 应用程序,默认的 ASP.NET 进程标识可将新的记录写入事件日志,但它的权限不足,无法创建新的事件源。要解决该问题,有两个方案可供选择。其一,您可以创建一种安装程序类,专门供管理员权限在安装时调用;其二,您可以配置 EventLog 注册表项的权限,允许 ASP.NET 进程标识(或模拟标识)在运行时创建事件源。建议您使用前一种方法。
• |
在安装时创建应用程序事件源
|
如果有现成的应用程序,且不希望创建安装程序类,您必须授予 ASP.NET 进程标识正确的访问权限,使其可访问事件日志注册表项。有关注册表项的详细信息和所需的正确访问权限,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全中的“事件日志”部分。
EventLogPermission
必须使用代码访问安全策略为写入事件日志的代码提供 EventLogPermission。如果配置 Web 应用程序在部分信任级别运行,便会出现问题。有关如何从部分信任 Web 应用程序向事件日志写入信息,请参阅模块 9 ASP.NET 代码访问安全性。
小结
本模块开始介绍了构建 Web 页和控件必须解决的主要威胁。很多应用程序级攻击都依赖于输入验证中的安全漏洞。多留意这方面的问题可确保验证策略更加完善,并正确验证在部分信任源中处理的所有数据。另一常见安全漏洞是,无法保护身份验证 cookie。本模块的表单身份验证部分介绍了几种行之有效的对策,可防止未经授权的访问攻击、会话劫持攻击和 cookie 重播攻击。
其他资源
有关详细信息,请参考下列资源:
• |
有关建立安全 Machine.config 和 Web.config 配置的信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web Services 的安全。 |
• |
有关可打印检查表的信息,请参阅本指南检查表部分的“检查表:保护 ASP.NET 的安全。 |
• |
有关确保开发人员工作站安全的相关信息,请参阅本指南的如何:保护开发人员工作站。 |
• |
有关 ASP.NET 中身份验证和授权的详细信息,请参阅“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications:Authentication, Authorization, and Secure Communication”中的模块 8“ASP.NET Security”,网址是:http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetch08.asp(英文)。 |
• |
有关使用表单身份验证的演练,请参阅“How To:Use Forms Authentication with SQL Server 2000”和“How To:Use Forms Authentication with Active Directory”,位于“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications: Authentication, Authorization, and Secure Communication”的“How To”部分,网址是 http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetHT00.asp(英文)。 |
• |
有关使用正则表达式的详细信息,请参阅 Microsoft 知识库中的文章 308252 How To:Match a Pattern by Using Regular Expressions and Visual C# .NET(英文)。 |
• |
有关 ASP.NET 中的用户输入验证详细信息,请参阅 MSDN 文章“User Input Validation in ASP.NET”,网址是:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/pdc_userinput.asp(英文)。 |
• |
有关 Secure cookie 属性的详细信息,请参阅 W3C 网站中的 RFC2109,网址是:http://www.w3.org/Protocols/rfc2109/rfc2109(英文)。 |
• |
有关 Open Hack 竞争方面的安全事项详细信息,请参阅 MSDN 文章构建和配置更安全的网站。 |