构建安全的 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 ArgumentExceptio