安全代码审查

安全代码审查

发布日期: 1/21/2005 | 更新日期: 1/21/2005
*

本单元概要

安全代码审查的重点是识别出可能引起安全问题和事故的不安全的编码技术和缺陷。虽然可能非常耗时,但是代码审查必须在项目开发周期中定期进行,因为在开发时修补安全缺陷的成本和工作量比以后(产品部署或者维护周期中)修补它们要小得多。

本单元有助于审查使用 Microsoft .NET Framework 构建的托管 ASP.NET Web 应用程序代码。本单元是按功能区域组织的,包含了若干部分,各部分都包含了完整的问题列表,这些列表提供了代码审查过程的方法和框架。

目标

使用本单元可以:

创建执行代码审查和 ASP.NET 安全审核的方法和框架。

找到代码中的跨站点脚本攻击缺陷。

找到代码中的 SQL 注入缺陷。

找到代码中潜在的缓冲区溢出缺陷。

根据一个完整的安全问题列表提问,快速定位安全问题。

评估特定于各种 .NET Framework 技术的安全问题。

识别恶意用户能够用来发起攻击的脆弱的编码技术。

了解如何使用 FxCop、文本搜索和 ILDASM 分析源代码和可用的 .NET 程序集。

适用范围

本单元适用于下列产品和技术:

Microsoft® Windows® Server 2000 和 Windows Server™2003 操作系统

Microsoft .NET Framework 1.1 和 ASP.NET 1.1

Microsoft SQL Server™ 2000

如何使用本单元

使用本单元创建或者扩展现有的代码审查过程。必须确保代码审查是开发周期中不可分隔的一部分,而且要注意这种审查的有效性与所分配的资源和预算数量是成正比的。

审查的目的是在部署代码之前识别尽可能多的潜在安全缺陷,因为在开发时修补安全缺陷所需的成本和工作量比以后在产品部署周期中修补它们要小得多。

要想充分利用本单元:

使用本指南第 III 部分中配套的“保护……”单元。参考这些单元可以为本单元中概述的审查问题提供更多信息。

“构建安全的程序集”

“代码访问安全实践”

“构建安全的 Web 服务”

“构建安全的数据访问”

使用配套的核对表:

核对表体系结构和设计审查

核对表保护 ASP.NET 的安全

核对表保护 Web 服务的安全

核对表保护企业服务的安全

核对表保护远程处理的安全

核对表保护数据访问的安全

本页内容
本单元概要本单元概要
目标目标
适用范围适用范围
如何使用本单元如何使用本单元
FxCopFxCop
执行文本搜索执行文本搜索
跨站点脚本攻击 (XSS)跨站点脚本攻击 (XSS)
SQL 注入攻击SQL 注入攻击
缓冲区溢出缓冲区溢出
托管代码托管代码
代码访问安全代码访问安全
非托管代码非托管代码
ASP.NET 页和控件ASP.NET 页和控件
Web 服务Web 服务
服务组件服务组件
远程处理远程处理
数据访问代码数据访问代码
小结小结
其他资源其他资源

FxCop

启动审查过程的一种好方式,是将已编译的程序集运行通过 FxCop 分析工具。这个工具可分析二进制的程序集(而非源代码),确保它们遵守 .NET Framework 设计准则,该准则可以在 MSDN 上找到。它还能检查您的程序集是否有强名称,这可提供防篡改和其他安全好处。该工具带有一组预定义的规则,但是您可以进行自定义,并扩展它们。

有关更多信息,请参阅下列资源:

要下载 FxCop 工具,请参阅 http://www.gotdotnet.com/team/libraries/default.aspx

要获取此工具的帮助和支持,请参阅 http://www.gotdotnet.com/community/messageboard/MessageBoard.aspx?ID=234

有关 FxCop 要检查的安全规则列表,请参阅 http://www.gotdotnet.com/team/libraries/FxCopRules/SecurityRules.aspx

执行文本搜索

为了辅助审查过程的进行,可以使用您熟悉的文本搜索工具在文件中定位字符串。这种工具使您可很快地定位有缺陷的代码。本单元中后面提出的许多审查问题都指出了查找特定缺陷时可以搜索的最佳字符串。

您可能已经有了惯用的搜索工具。如果没有,您可以使用 Visual Studio .NET 中的 Find in Files 工具,或者 Findstr 命令行工具,后者包含在 Microsoft Windows 操作系统中。

如果您使用 Windows 资源管理器中的 Windows XP 搜索工具,并且使用 A word or phrase in the file 选项,请检查您是否安装了最新的 Windows XP Service Pack,否则搜索可能失败。有关更多信息,请参阅 Microsoft 知识库文章 309173,“Using the "A Word or Phrase in the File" Search Criterion May Not Work”。

搜索硬编码的字符串

在您对源代码进行逐行的详细分析之前,首先快速搜索整个基本代码,识别硬编码的密码、帐户名和数据库连接字符串。扫描代码并搜索常见的字符串模式,如以下的一些:“key”、“secret”、“password”、“pwd”和“connectionstring”。

例如,要在应用程序 Web 目录中搜索字符串“password”,应该从命令提示符使用 Findstr 工具,如下所示:

Findstr /S /M /I /d:c:/projects/yourweb "password" *.*
  

Findstr 使用以下命令行参数:

/S — 包括子目录。

/M — 只列出文件名。

/I — 使用不区分大小写的搜索。

/D:dir — 搜索分号分隔的目录列表。如果要搜索的文件路径包含空格,应该用双引号将路径括起来。

自动执行 Findstr

您可以用常见搜索字符串创建一个文本文件。Findstr 然后可以从文本文件中读取搜索的字符串,如下所示。从包含 .aspx 文件的目录运行以下命令。

Findstr /N /G:SearchStrings.txt *.aspx
  

/N 在发现匹配时输出对应的行号。/G 指示包含搜索字符串的文件。在本例中,搜索所有 ASP.NET 页 (*.aspx) 中是否有 SearchStrings.txt 所包含的字符串。

ILDASM

您还可以将 Findstr 命令与 ildasm.exe 实用工具结合使用,来搜索二进制程序集中硬编码的字符串。以下命令使用 ildasm.exe 搜索 ldstr 中间语言语句,它可识别字符串常量。请注意下面显示的输出暴露了硬编码的数据库连接和著名的 sa 帐户的密码。

Ildasm.exe secureapp.dll /text | Findstr ldstr
      IL_000c:  ldstr      "RegisterUser"
      IL_0027:  ldstr      "@userName"
      IL_0046:  ldstr      "@passwordHash"
      IL_0065:  ldstr      "@salt"
          IL_008b:  ldstr      "Exception adding account. "
      IL_000e:  ldstr      "LookupUser"
      IL_0027:  ldstr      "@userName"
          IL_007d:  ldstr      "SHA1"
          IL_0097:  ldstr      "Exeception verifying password. "
      IL_0009:  ldstr      "SHA1"
      IL_003e:  ldstr      "Logon successful: User is authenticated"
      IL_0050:  ldstr      "Invalid username or password"
      IL_0001:  ldstr      "Server=AppServer;database=users; username='sa'  
                            password=password"
  

Ildasm.exe 位于 /Program Files/Microsoft Visual Studio .NET 2003/SDK/v1.1/bin 文件夹中。有关所支持的更多命令行参数信息,可以运行 ildasm.exe /?

跨站点脚本攻击 (XSS)

只要代码在返回客户端的 HTML 输出流中使用输入参数,就很容易受到跨站点脚本(XSS,也称 CSS)攻击。即使在进行代码审查之前,您也可以运行一个简单的测试,来检查应用程序是否存在 XSS 缺陷。搜索那些将用户输入信息发送回浏览器的页。

XSS 错误是在用户输入的数据中保持太多信任的一个例子。例如,应用程序可能期望用户输入一个定价,但是攻击者会在定价之外包含一些 HTML 和 JavaScript 代码。所以,您应该总是确保对来自不可信来源的数据进行验证。在审查代码时,总是询问这样的问题:“这个数据进行验证了吗?”在 ASP.NET 应用程序中保存所有入口点(如 HTTP 头、查询字符串、窗体数据等等)的列表,并确保所有输入都会在某点受到有效性检查。不要测试输入值是否不正确,因为这种方法认为您会留意所有存在潜在危险的输入。ASP.NET 应用程序中最常见的检查数据有效性的方式是使用正则表达式。

您可以通过在窗体字段中键入一些文字(如“XYZ”)并测试输出,来进行简单的测试。如果浏览器显示“XYZ”或者如果查看 HTML 源文件时看到的是“XYZ”,那么您的 Web 应用程序将很容易受到 XSS 攻击。如果您需要看到更动态的内容,需要注入 <script>alert('hello');</script>。这种技术可能无法在所有情况下都适用,因为它取决于如何使用输入来生成输出。以下过程有助于识别常见的 XSS 缺陷:

标识输出输入的代码

标识有可能存在危险的 HTML 标记和属性

标识处理 URL 的代码

检查输出是否编码

检查字符编码是否正确

检查 validateRequest 属性

检查HttpOnly cookie 选项

检查 <frame> 安全属性

检查是否使用 innerText 和 innerHTML 属性

标识输出输入的代码

从浏览器查看页输出的源代码,看是否您的代码位于一个属性中。如果确实如此,注入以下代码并重新测试,以查看输出。

"onmouseover= alert('hello');"
  

开发人员常用的一个技术是筛选 < and > 字符。如果所审查的代码筛选了这些字符,那么可以改用以下代码测试:

&{alert('hello');}
  

如果代码不筛选这些字符,那么可以通过使用以下脚本测试代码:

<script>alert(document.cookie);</script>;

在使用这个脚本之前可能必须要添加一个结束标记,如下所示。

"></a><script>alert(document.cookie);</script>

搜索“.Write”

在 .aspx 源代码和任何其他为应用程序开发的程序集包含的代码中搜索“.Write”字符串。这将定位所有出现的 Response.Write,以及任何可能通过响应对象变量生成输出的内部例程,如下面所示的代码。

public void WriteOutput(Response respObj)
{
   respObj.Write(Request.Form["someField"]);
}
  

您还应该在 .aspx 源代码中搜索 “<%=”字符串,这也可以用来写输出,如下所示:

<%=myVariable %>

下表列出了一些常见的结合使用输入字段和 Response.Write 的情况。

表 1 输入的可能来源
输入来源示例

窗体字段

Response.Write(name.Text); Response.Write(Request.Form["name"]);

查询字符串

Response.Write(Request.QueryString["name"]);

Cookie

Response.Write(      Request.Cookies["name"].Values["name"]);

会话变量和应用程序变量

Response.Write(Session["name"]); Response.Write(Application["name"]);

数据库和数据存储区

SqlDataReader reader = cmd.ExecuteReader();Response.Write(reader.GetString(1));

标识有可能存在危险的 HTML 标记和属性

虽然并不全面,但是以下常用的 HTML 标记可能使恶意用户可注入脚本代码:

<applet>

<body>

<embed>

<frame>

<script>

<frameset>

<html>

<iframe>

<img>

<style>

<layer>

<ilayer>

<meta>

<object>

HTML 属性(如 srclowsrcstylehref)可以与以上的标记结合使用,导致 XSS 攻击。

例如,<img> 标记的 src 属性可能是注入的来源,如下例中所示。

<IMG SRC="javascript:alert('hello');">
<IMG SRC="java&#010;script:alert('hello');">
<IMG SRC="java&#X0A;script:alert('hello');">
  

<style> 标记还能通过改变 MIME 类型,成为注入的来源,如下所示。

<style TYPE="text/javascript">
alert('hello');
</style>

检查是否您的代码试图通过筛选掉一些已知的危险字符来净化输入。不要依赖此方法,因为恶意用户一般可找到替代的表示方法绕过验证。相反,您的代码应该对已知安全的输入进行验证。下表列出了表示一些常用字符的若干方式:

表 2 字符表示
字符十进制十六进制HTML 字符集Unicode

"(双引号)

&#34

&#x22;

"

/u0022

'(单引号)

&#39

&#x27;

&apos;

/u0027

&(和号)

&#38;

&#x26

&amp;

/u0026

<(小于号)

&#60

&#x3C;

&lt;

/u003c

>(大于号)

&#62

&#x3E;

&gt;

/u003e

标识处理 URL 的代码

处理 URL 的代码可能存在缺陷。审查代码,看看是否容易受到以下常见攻击:

如果您的 Web 服务器没有使用最新的安全修补程序更新,它可能遭到目录遍历和双斜杠攻击,如:

http://www.YourWebServer.com/..%255%../winnt
http://www.YourWebServer.com/..%255%..//somedirectory
  

如果您的代码筛选了“/”,攻击者可通过使用同一字符的替代表示形式,很容易地绕过筛选器。例如,“/”的超长 UTF–8 表示形式是“%c0f%af”,可以用在以下 URL 中:

http://www.YourWebServer.com/..%c0f%af../winnt
  

如果您的代码处理查询字符串输入,应该检查它是否对输入数据进行了限制并执行了边界检查。检查如果攻击者通过查询字符串参数传递非常多的数据,代码是否不易受到攻击。

http://www.YourWebServer.com/test.aspx?var=InjectHugeAmountOfDataHere
  

检查输出是否编码

您应该检查是否使用 HtmlEncode 对包含所有类型输入的 HTML 输出进行了编码,虽然这不能替代对输入是否正确和输入的格式是否规范的检查。还要检查是否使用了 UrlEncode 来编码 URL 字符串。输入数据可能来自查询字符串、窗体字段、cookie、HTTP 头和从数据库中读取的输入,尤其是在与其他应用程序共享数据库时。通过对数据进行编码,可以防止浏览器将 HTML 视为可执行脚本。

检查字符编码是否正确

为了帮助防止攻击者使用规范化和多字节转义序列欺骗输入验证例程,应该检查字符编码是否已经正确设置以限制表示输入的方式。

检查应用程序的 Web.config 文件是否已经设置由 <globalization> 元素配置的 requestEncodingresponseEncoding 属性,如下所示。

<configuration>
   <system.web>
      <globalization 
         requestEncoding="ISO-8859-1"
         responseEncoding="ISO-8859-1"/>
   </system.web>
</configuration>
  

字符编码还可以使用 <meta> 标记或者 ResponseEncoding 页级属性在页级设置,如下所示。

<% @ Page ResponseEncoding="ISO-8859-1" %>

有关更多信息,请参阅“构建安全的 ASP.NET 页和控件”单元。

检查 validateRequest 属性

使用 .NET Framework 1.1 版构建的 Web 应用程序执行输入筛选,以消除潜在的恶意输入,如嵌入脚本。但不要依赖这一点,应该使用它作为纵深防范措施。检查配置文件中的 <pages> 元素,确认 validateRequest 属性是否已经设置为 true。这还可以设置为页级属性。扫描 .aspx 源文件中的 validateRequest,检查它没有为任何页设置为 false

检查 HttpOnly cookie 选项

Internet Explorer 6 SP 1 支持一个新的 HttpOnly cookie 属性,可以防止客户端脚本从 document.cookie 属性访问 cookie。这样返回的将是一个空的字符串。无论何时用户浏览到当前域中的 Web 站点,仍然发送 cookie 到服务器。有关更多信息,请参阅“构建安全的 ASP.NET 页和控件”单元中的“跨站点脚本攻击”部分。

检查 安全属性

Internet Explorer 6 和更高版本支持 <frame> 和 <iframe> 元素的一个新属性 security。您可以使用 security 属性将用户的受限站点 Internet Explorer 安全区域设置应用于单独的 frame 或者 iframe。有关更多信息,请参阅“构建安全的 ASP.NET 页和控件”单元中的“跨站点脚本攻击”部分。

检查是否使用 innerText 和 innerHTML 属性

如果您创建了一个带有不可信输入的页,应该验证是否使用了 innerText 属性而非 innerHTMLinnerText 属性可安全呈现内容并确保不会执行脚本。

更多信息

有关 XSS 的更多信息,请参阅以下文章:

“CSS Quick Start:What Customers Can Do to Protect Themselves from Cross-Site Scripting,”,网址是:http://www.microsoft.com/technet/security/news/crsstQS.asp

Microsoft 知识库文章 252985,“How To:Prevent Cross-Site Scripting Security Issues 。”

“CERT Advisory CA–2000–02, Malicious HTML Tags Embedded in Client Web Requests,”,在 CERT/CC Web 站点上,网址是:http://www.cert.org/advisories/CA-2000-02.html

“Understanding Malicious Content Mitigation for Web Developers,”,在 CERT/CC Web 站点上,网址是:http://www.cert.org/tech_tips/malicious_code_mitigation.html

SQL 注入攻击

只要代码使用输入参数构造 SQL 语句,将很容易受到 SQL 注入攻击。与 XSS 错误一样,SQL 注入攻击是因为对用户输入过分信任,而且没有验证输入是否正确和格式是否规范而造成的。

以下过程有助于定位 SQL 注入缺陷:

1.

查找访问数据库的代码

扫描字符串“SqlCommand”、“OleDbCommand”或者“OdbcCommand”。

2.

检查代码是否使用了参数化存储过程

单靠存储过程是无法防止 SQL 注入攻击的。应该使用参数化存储过程检查您的代码。使用类型化参数对象(如 SqlParameterOleDbParameter 或者 OdbcParameter)检查您的代码。下例说明了 SqlParameter 的使用:

SqlDataAdapter myCommand = new SqlDataAdapter("spLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add(
                                "@userName", SqlDbType.VarChar,12);
parm.Value=txtUid.Text;
  

类型化 SQL 参数将检查输入的类型和长度,确保 userName 输入值在数据库中当作文本值而不是可执行代码进行处理。

3.

使用 SQL 语句中的参数检查您的代码

如果不使用存储过程,可以使用它构造的 SQL 语句参数检查代码,如下例中所示:

select status from Users where UserName=@userName
  

检查是否没有 使用以下方法,即通过字符串串联直接用输入构造可执行 SQL 语句:

string sql = "select status from Users where UserName='" 
           + txtUserName.Text + "'";
  

4.

检查代码是否试图筛选输入

一个很常见的方法是开发筛选例程将转义符添加到对 SQL 有特殊意义的字符上。这种方法是不安全的,您不应该依赖它,因为存在字符表示形式问题。

缓冲区溢出

审查代码是否存在缓冲区溢出时,将审查工作主要放在通过 P/Invoke 或者 COM 互操作层调用非托管代码的代码上。托管代码本身受到缓冲区溢出攻击的可能性很小,因为无论是否访问数组,系统都将自动检查数组边界。只要您调用了 Win32 DLL 或者 COM 对象,都应该严格检查 API 调用。

以下过程有助于定位缓冲区溢出缺陷:

1.

定位对非托管代码的调用

扫描源文件中的“System.Runtime.InteropServices”,这是调用非托管代码时使用的命名空间名称。

2.

检查传给非托管 API 的字符串参数

这些参数是缓冲区溢出的主要来源。检查您的代码,检查所有输入字符串的长度,验证它是否没有超过 API 定义的限制。如果非托管 API 接受字符指针,您可能无法知道最大允许字符串长度,除非有非托管源代码的访问权限。常见缺陷如以下代码片段所示:

void SomeFunction( char *pszInput )
{
  char szBuffer[10];
  // Look out, no length checks. Input is copied straight into the buffer
  // Should check length or use strncpy.
  strcpy(szBuffer, pszInput);
  . . .
}
  

如果您使用 strncpy,缓冲区溢出仍然可能发生,因为它不检查目标字符串中是否有足够空间,只限制复制字符的数量。

如果您因为并不拥有非托管代码而无法检查它,那么就通过故意传入长的输入字符串和无效参数严格测试 API。

3.

检查文件路径长度。

如果非托管 API 接受文件名和路径,那么应该检查您的包装方法,检查文件名和路径是否没有超过 260 个字符。这是通过 Win32 MAX_PATH 常数定义的。还应该注意目录名和注册表项可能的最大字符数是 248。

4.

检查输出字符串。

检查是否代码使用了 StringBuilder 接收从非托管 API 传回的字符串。检查 StringBuilder 的容量是否足够大,可容纳非托管 API 传回的最长字符串,因为非托管代码传回的字符串可能是任意长度的。

5.

检查数组边界。

如果您使用数组将输入传递到非托管 API,应该检查托管包装,验证是否没有超过数组容量。

6.

检查您的非托管代码是否用 /GS 开关进行编译。

如果您拥有非托管代码,使用 /GS 开关启用堆栈探测,检测是否存在某些类型的缓冲区溢出。

托管代码

使用本部分中的审查问题分析整个托管源代码库。这些审查问题无论程序集类型如何都适用。本部分有助于识别常见的托管代码缺陷。有关本部分中所提出问题的更多信息和说明缺陷的代码示例,请参阅“构建安全的程序集”单元。

如果您的托管代码使用了显式代码访问安全功能,请参阅本单元后面“代码访问安全”中的其他审查点。以下审查问题有助于识别托管代码缺陷:

您的类设计安全吗?

创建线程了吗?

使用序列化了吗?

使用反射了吗?

处理异常了吗?

使用加密技术了吗?

存储机密了吗?

使用委托了吗?

您的类设计安全吗?

程序集的安全性取决于所包含的类和其他类型。以下问题有助于审查类设计的安全性:

限制类型和成员可见性了吗?

审查任何标记为 public 的类型或者成员,检查它是否是程序集公共接口的所需部分。

非基类密封了吗?

如果您不希望从一个类中派生,使用 sealed 关键字防止代码被潜在的恶意子类滥用。

对于公共基类,您可以使用代码访问安全继承要求来限制可从类中继承的代码。这是一种很好的纵深防范措施。

使用属性公开字段了吗?

检查您的类,不要直接公开字段。使用属性公开非私有字段。这使您可验证输入值和应用其他安全检查。

使用只读属性了吗?

验证是否有效地使用了只读属性。如果字段设计为不能进行设置,应该通过只提供 get 访问器实现只读属性。

使用虚拟内部方法了吗?

这些方法可以从访问类的其他程序集改写。如果并无如此要求的话,使用声明性检查或者删除 virtual 关键字。

实现 IDisposable 了吗?

如果是这样的话,检查当您完成对象实例时是否调用了 Dispose 方法,以确保所有资源被释放。

创建线程了吗?

多线程代码容易出现与时间相关的细微错误,或者出现可能导致安全缺陷的竞争情形。要定位多线程代码,搜索源代码中的文本“Thread”,以识别新的 Thread 对象是在哪里创建的,如以下代码片段中所示:

Thread t = new Thread(new ThreadStart(someObject.SomeThreadStartMethod));
  

以下审查问题有助于识别潜在的多线程处理缺陷:

您的代码缓存了安全检查的结果吗?

您的代码如果缓存了安全检查的结果(例如在静态或者全局变量中),然后使用此标志进行后续的安全决策,将非常容易出现竞争情形。

您的代码模拟吗?

线程创建了新的当前模拟线程吗?新的线程总是使用进程级安全上下文,而不是现有线程的安全上下文。

您的代码包含静态类构造函数吗?

检查静态类构造函数,检查它们在有两个或者多个线程同时访问时,是否不会出现问题。如果必要,同步线程以防止出现这种情况。

同步 Dispose 方法了吗?

如果对象的 Dispose 方法没有同步,有可能两个线程会执行同一对象的 Dispose。这可带来安全问题,尤其若清理代码释放了非托管资源处理程序(如文件、进程或者线程句柄)更是如此。

使用序列化了吗?

支持序列化的类要么用 SerializableAttribute 标记,要么派生自 ISerializable。要定位支持序列化的类,请执行对“Serializable”字符串的文本搜索。然后,审查您的代码是否有以下问题:

类包含敏感数据吗?

如果是这样的话,检查代码,防止通过用 [NonSerialized] 属性标记敏感数据或者实现 ISerializable 然后控制序列化哪个字段,来序列化敏感数据。

如果您的类需要序列化敏感数据,审查如何保护数据。首先考虑加密数据。

类实现 ISerializable 了吗?

如果是这样的话,您的类是否只支持完全信任调用方呢,如由于它安装在具有强名称不包含 AllowPartiallyTrustedCallersAttribute 的程序集中?如果您的类支持部分信任调用方,检查 GetObjectData 方法的实现是否通过使用适当的权限要求授权了呼叫代码。一个较好的技术是使用一个 StrongNameIdentityPermission 要求限制哪些程序集可序列化您的对象。

类验证数据流了吗?

如果您的代码包括一个接收序列化数据流的方法,应该检查每个字段是否在从数据流中读取时进行了验证。

使用反射了吗?

为了辅助定位使用反射的代码,搜索“System.Reflection”— 这是包含反射类型的命名空间。如果您确实使用了反射,审查以下问题以辅助识别潜在的缺陷:

动态加载程序集了吗?

如果您的代码加载程序集,以创建对象实例并调用类型,它需要从输入数据获取程序集或者类型名称吗?如果是这样的话,检查代码是否使用权限要求进行了保护,确保所有呼叫代码得到授权。例如,使用 StrongNameIdentity 权限要求或者要求完全信任。

在运行时动态创建代码了吗?

如果您的程序集动态生成代码为调用方执行操作,检查调用方是否无法影响生成的代码。例如,您的代码生成依赖调用方提供的输入参数吗?这是应该避免的,如果它非常必要,也应该确保对输入进行验证,而且它不能用来对代码生成产生负面影响。

对其他类型使用反射了吗?

如果是这样的话,检查是否只有可信代码可调用您的代码。使用代码访问安全权限要求授权呼叫代码。

处理异常了吗?

安全的异常处理对于健全的代码而言是必需的,可以确保记录足够的异常详细信息,这样有助于问题的诊断以及防止内部系统详细信息暴露给客户端。审查以下问题,有助于识别潜在的异常处理缺陷:

有过早失败的问题吗?

检查您的代码是否有过早失败问题,避免进行不必要的消耗资源的处理。如果您的代码确实失败了,检查生成的错误是否不允许用户绕过安全检查运行特权代码。

如何处理异常?

避免暴露系统或者应用程序详细信息给调用方。例如,不要将调用堆栈返回给最终用户。使用 try/catch 块包装可能生成异常的资源访问或者操作。只处理知道如何处理的异常,避免用一般性的包装包装特定异常。

记录异常详细信息了吗?

检查异常详细信息是否在异常来源进行了记录,这样有助于问题诊断。

使用异常筛选器了吗?

如果是这样的话,注意在调用堆栈较高处的筛选器的代码可在 finally 块的代码之前运行。检查您是否不依赖 finally 块中的状态变化,因为状态变化将不会在异常筛选器执行之前发生。

有关异常筛选器缺陷的例子,请参阅“构建安全的程序集”单元中的“异常管理”。

使用加密技术了吗?

如果是这样的话,检查您的代码,不要实现自己的加密例程。相反,代码应该使用 System.Security.Cryptography 命名空间或者使用 Win32 加密(如数据保护应用程序编程接口 (DPAPI))。审查以下问题有助于识别潜在的与加密相关的缺陷:

使用对称加密了吗?

如果是这样的话,检查在加密的数据需要持久存储较长时间时是否使用了 Rijndael(现在称为高级加密标准 [AES])或者三重数据加密标准(3DES)。较脆弱(但是更快)的 RC2 和 DES 算法只能用来加密生命期较短的数据,如会话数据。

使用尽可能最大的密钥大小了吗?

使用所使用的算法可能的最大密钥大小。更大的密钥大小将使对密钥的攻击困难得多,但是会降低性能。

使用散列了吗?

如果是这样的话,检查您是否在需要使用主体证明它知道与您共享的机密时使用了 MD5 和 SHA1。例如,质询-响应身份验证系统使用散列证明客户端知道密码,而无需让客户端传递密码给服务器。使用带有消息身份验证代码 (MAC) 的 HMACSHA1,它要求您和客户端共享密钥。可以提供完整性检查和一定程度的身份验证。

为加密目的生成随机数了吗?

如果是这样的话,检查您的代码是否使用了 System.Security.Cryptography.RNGCryptoServiceProvider 类而不是 Random 类生成随机数。Random 类生成的并不是真正不可重复和预测的随机数。

存储机密了吗?

如果您的程序集中需要存储机密,应该审查设计,检查是否绝对必要存储机密。如果必须存储机密,审查以下问题以尽可能安全地存储:

是否在内存中存储机密?

不要以明文在内存中存储机密较长时间。从存储区中检索机密,将其解密,使用它,然后用零代替存储机密的空间中的值。

是否在 Web.config 或者 Machine.config 中存储明文密码或者 SQL 连接字符串?

不要这样做。使用 aspnet_setreg.exe 将加密的凭据存储在注册表中的 <identity>、<processModel> 和 <sessionState>元素中。有关获取和使用 Aspnet_setreg.exe 的信息,请参阅 Microsoft 知识库文章 329290,“How To:Use the ASP.NET Utility to Encrypt Credentials and Session State ”。

如何加密机密?

检查代码是否使用 DPAPI 加密连接字符串和凭据。不要在本地安全机构 (LSA) 中存储机密,因为用来访问 LSA 的帐户要求扩展的特权。有关使用 DPAPI 的信息,请参阅“如何:创建 DPAPI 库”,在“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications:”中的“如何做”部分,网址是:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetHT07.asp

在注册表中存储机密了吗?

如果是这样的话,检查是否首先加了密然后用受限的 ACL 进行了保护(如果它们存储在HKEY_LOCAL_MACHINE 中)。ACL 并非必需,如果代码使用 HKEY_CURRENT_USER,因为这对于在相关用户帐户下运行的进程是自动受限的。

您担心反向工程吗?

如果是这样的话,考虑使用混淆工具。有关更多信息,请参阅 http://www.gotdotnet.com/team/csharp/tools/default.aspx 列出的混淆工具列表。

不要依赖混淆工具隐藏机密数据。混淆工具只能使识别机密数据更困难,但是无法解决问题。

使用委托了吗?

任何代码都可以将一个方法与一个委托关联起来。这包括以比您的代码还低的信任级运行的潜在恶意的代码。

从不可信来源接受委托吗?

如果是这样的话,检查您是否通过使用带 SecurityAction.PermitOnly 的安全权限,限制了委托方法能够获得的代码访问权限。

在调用委托前使用断言了吗?

应该避免这样做,因为您不知道委托代码将会在调用它之前做什么。

代码访问安全

所有托管代码都要受到代码访问安全权限要求的限制。许多问题只有在您的代码在部分信任环境中使用时,或者当您的代码或者呼叫代码没有通过代码访问安全策略授予完全信任时,才会变得显而易见。

有关本部分中所提出问题的更多信息,请参阅“代码访问安全实践”单元。

使用以下审查点检查您使用代码访问安全是否合适和安全:

支持部分信任调用方吗?

限制访问公共类型和成员吗?

使用声明性安全了吗?

调用 Assert 了吗?

当需要的时候使用权限要求了吗?

使用链接要求了吗?

使用 Deny 或者 PermitOnly 了吗?

使用特别危险的权限了吗?

使用 /unsafe 选项编译了吗?

支持部分信任调用方吗?

如果您的代码支持部分信任调用方,它受到攻击的可能性要大得多,因此执行全面和彻底的代码审查尤其重要。审查 Web 应用程序中的 <trust> 级配置设置,看是否运行在部分信任级。如果确实如此,为应用程序开发的程序集需要支持部分信任调用方。

以下问题有助于识别存在潜在的易受攻击的区域:

您的程序集具有强名称吗?

如果确实如此,则默认安全策略可确保它无法被部分信任的调用方调用。公共语言运行库 (CLR) 会发出完全信任的隐式链接要求。如果您的程序集不具有强名称,它可以被任何代码调用,除非采用明确的步骤(例如通过显式要求完全信任)限制调用方。

ASP.NET 应用程序调用的具有强名称的程序集必须安装在全局程序集缓存中。

使用 APTCA 了吗?

如果具有强名称的程序集包含 AllowPartiallyTrustedCallersAttribute,部分信任的调用方将可调用您的代码。在此情况下,检查任何资源访问或者程序集执行的其他特权操作,是否得到授权以及是否通过其他代码访问安全要求进行了保护。如果您使用 .NET Framework 类库访问资源,完整的堆栈审核要求是自动发出的,将授权呼叫代码,除非您的代码使用 Assert 调用防止堆栈审核。

返回对象引用了吗?

检查方法返回值和 ref 参数,看您的代码是在哪里返回对象引用的。检查您的部分信任代码是否不会返回从要求完全信任调用方的程序集获取的对象的引用。

限制访问公共类型和成员吗?

您可以使用代码访问安全标识要求限制访问公共类型和成员。这是一种很有用的减少程序集受攻击面的方法。

通过使用标识要求限制调用方?

如果您有类或者结构只想在特定应用程序中由特定程序集使用,可以通过标识要求限制调用方的范围。例如,您可以使用带有 StrongNameIdentityPermission 的要求将调用方限制为一个特定的程序集集合,它们都使用与要求中公钥对应的私钥进行了签名。

使用继承要求限制子类了吗?

如果您知道只有特定代码应该从一个基类中继承,应该检查该类是否使用了带有 StrongNameIdentityPermission 的继承要求。

使用声明性安全属性了吗?

声明性安全属性可用工具(如 Permview.exe)显示。这极大地帮助了程序集的客户和管理员理解代码的安全需求。

请求最低权限了吗?

搜索“.RequestMinimum”字符串,查看您的代码是否使用权限请求指定了其最低权限需求。您应该这样清楚地记载程序集的权限需求。

请求可选或者拒绝权限了吗?

搜索“.RequestOptional”和“.RequestRefuse”字符串。如果您使用这两个操作的任何一个开发最低特权代码,请小心您的代码可能无法再调用具有强名称的程序集了,除非它们用 AllowPartiallyTrustedCallersAttribute 进行了标记。

使用命令性安全代替声明性安全了吗?

有时候代码中的命令性检查是必要的,因为需要应用逻辑决定应该要求哪些权限,或者因为您需要在要求中有运行时变量。如果您不需要特定逻辑,请考虑使用声明性安全记载程序集的权限需求。

混合类和成员级属性了吗?

不要这样做。成员特性,例如方法的成员特性或者属性的成员特性,将替换具有同样的安全操作的类级特性,而不是与它们结合起来。

调用 Assert 了吗?

扫描代码搜索 Assert 调用。这可能找到 Debug.Assert 的实例。查找代码调用 CodeAccessPermission 对象的 Assert 的地方。当您断言代码访问权限时,实际上绕过了代码访问安全权限要求的堆栈审核,这是一种非常危险的做法。您的代码要采取什么步骤才能确保恶意调用方不能利用断言访问受到保护的资源或者特权操作呢?查看下列问题:

使用了要求、断言模式吗?

检查您的代码是否在 Assert 之前发出 Demand。代码应该要求更细粒度的权限在断言更宽的权限(如非托管代码权限)之前授权调用方。

Assert 调用匹配 RevertAssert 了吗?

检查 Assert 的每次调用是否与 RevertAssert 的调用匹配。当调用 Assert 的方法返回时,将隐式地删除 Assert,但是尽可能快地在 Assert 调用之后显式调用 RevertAssert,是好的做法。

减少断言时间长度了吗?

检查是否只断言权限持续存在最小必需时间长度。例如,如果只是在调用另一个方法的时候需要使用 Assert 调用,可以检查是否在方法调用之后立即调用了 RevertAssert

在需要的时候使用权限要求了吗?

您的代码总是会受到来自 .NET Framework 类库权限要求检查的限制,但是如果代码使用显式权限要求,应该检查是否也相应地受到了限制。搜索代码中的“.Demand”字符串以识别声明性和命令性权限要求,然后审查以下问题:

缓存数据了吗?

如果是这样的话,检查代码在访问缓存数据之前是否发出了合适的权限要求。例如,如果数据是从一个文件中获取的,您需要确保呼叫代码得到授权从填充缓存的地方访问文件,在访问缓存数据之前要求一个 FileIOPermission

公开自定义资源或者特权操作了吗?

如果您的代码通过非托管代码公开自定义资源或者特权操作,应该检查它是否发出了适当的权限要求,这可能是一个内置的权限类型或者是一个自定义权限类型,具体取决于资源的性质。

要求足够快吗?

检查您是否在访问资源或者执行特权操作之前发出了一个权限要求。不要在访问资源之后才授权调用方。

发出冗余要求了吗?

使用 .NET Framework 类库的代码受权限要求的限制。您的代码不需要发出同样的要求。这将导致重复和浪费的堆栈审核。

使用链接要求了吗?

链接要求与常规要求不同,只检查直接调用方。它们不执行完整的堆栈审核,因此使用链接要求的代码容易受到引诱攻击。有关引诱攻击的信息,请参阅“代码访问安全实践”单元中的“链接要求”。

搜索代码中的“.LinkDemand”字符串以识别使用链接要求的地方。它们只能声明性地使用。以下代码片段举出了一个例子:

 [StrongNameIdentityPermission(SecurityAction.LinkDemand, 
                                PublicKey="00240000048...97e85d098615")]
public static void SomeOperation() {}
  

有关本部分中所提问题的更多信息,请参阅“代码访问安全实践”单元中的“链接要求”。以下问题有助于审查代码中链接要求的使用:

为什么要使用链接要求?

一种防范方法是尽可能避免使用链接要求。不要仅仅因为要提高性能和消除完整的堆栈审核而使用它们。与其他 Web 应用程序的性能问题如网络延迟和数据库访问相比,堆栈审核的代价很小。链接要求只有在您知道而且可限制哪些代码可调用您的代码时才是安全的。

信任调用方吗?

当您使用链接要求时,需要依赖调用方防止引诱攻击。链接要求只有在知道而且可准确限制代码的直接调用方时才是安全的,您可以信任这些调用方授权它们的调用方。

调用了使用链接要求保护的代码吗?

如果是这样的话,您的代码是否通过要求代码调用方的安全权限提供授权呢?传给您的方法的参数可传递到调用的代码吗?如果是这样的话,它们能够恶意影响调用的代码吗?

在方法级和类级使用链接要求吗?

当您在方法中添加链接要求时,它将改写类的链接要求。检查方法是否也包含了类级链接要求。

使用未密封类的链接要求了吗?

链接要求不能被派生类型继承,也不能在调用派生类型的改写方法时使用。如果您改写一个需要用链接要求保护的方法,对改写方法应用链接要求。

使用链接要求保护结构了吗?

链接要求并不能防止不可信调用方构造结构。这是因为默认构造函数不会为结构自动生成,所以结构级链接要求只适用于使用显式构造函数的时候。

使用显式接口了吗?

搜索 Interface 关键字来查找显式接口。如果使用了显式接口,应该检查是否使用链接要求标记了方法实现。如果使用了,还需检查接口定义是否包含同样的链接要求。否则,调用方可能绕过链接要求。

使用有可能存在危险的权限了吗?

检查以下权限类型是否只授予高度可信代码。它们大部分都不拥有自己的专用权限类型,而是使用一般性的 SecurityPermission 类型。您应该严格检查使用这些类型的代码,确保可将风险降低至最小。同样,您必须有很充分的理由才能使用这些权限。

表 3 危险的权限
权限说明

SecurityPermission.UnmanagedCode

代码可调用非托管代码。

SecurityPermission.SkipVerification

程序集中的代码不再需要验证类型安全性。

SecurityPermission.ControlEvidence

代码可提供其自己的证据供安全策略评估使用。

SecurityPermission.ControlPolicy

代码可查看和更改策略。

SecurityPermission.SerializationFormatter

代码可使用序列化。

SecurityPermission.ControlPrincipal

代码可操作用于授权的主体对象。

ReflectionPermission.MemberAccess

代码可通过反射调用一个类型的私有成员。

SecurityPermission.ControlAppDomain

代码可创建新的应用程序域。

SecurityPermission.ControlDomainPolicy

代码可改变域策略。

使用 /unsafe 选项编译了吗?

使用 Visual Studio .NET 检查项目属性,查看是否 Allow Unsafe Code Blocks 设置为 true。这将设置 /unsafe 编译器标志,它告知编译器代码包含不安全的块,并请求程序集具有最低的 SkipVerification 权限。如果您用 /unsafe 选项编译,应该审查为什么需要如此。如果理由合理,要格外小心地审查源代码中潜在的缺陷。

非托管代码

应该对调用非托管代码(包括 Win32 DLL 和 COM 对象)的代码给予特别的注意,因为它们会增加安全风险。非托管代码的类型安全是无法验证的,而且会引入可能的缓冲区溢出。非托管代码的资源访问不受代码访问安全检查的限制。这是由托管包装类负责的。

通常,您不应该直接将非托管代码公开给部分信任的调用方。有关本部分所提出问题的更多信息,请参阅“构建安全的程序集”单元和“代码访问安全实践”单元中“非托管代码”部分。

使用以下审查问题验证非托管代码的使用:

断言非托管代码权限了吗?

如果是这样的话,在调用 Assert 方法以确保所有调用方都得到授权访问资源或者非托管代码公开的操作之前,检查您的代码是否要求了合适的权限。例如,以下代码片段说明了如何要求自定义加密权限,然后断言非托管代码权限:

// Demand custom EncryptionPermission.
(new EncryptionPermission(
        EncryptionPermissionFlag.Encrypt, storeFlag)).Demand();
// Assert the unmanaged code permission.
(new SecurityPermission(SecurityPermissionFlag.UnmanagedCode)).Assert();
// Now use P/Invoke to call the unmanaged DPAPI functions.
  

有关更多信息,请参阅“代码访问安全实践”单元中的“Assert 和 RevertAssert”部分。

使用了 SuppressUnmanagedCodeAttribute 吗?

此属性会取消在托管代码调用非托管代码时自动发出的对非托管代码权限的要求。如果 P/Invoke 方法或者 COM 互操作接口是用这个属性批注的,应该确保所有将导向非托管代码调用的代码路径用安全权限要求进行了保护,从而对调用方进行授权。还应该检查这个属性是用在方法级而非类级。

添加 SupressUnmanagedCodeSecurityAttribute 会使互操作层发出的 UnmanagedCode 权限隐式要求变为 LinkDemand。您的代码容易受到引诱攻击。

非托管入口点公开可见吗?

检查您的非托管代码入口点是否标记为 private 或者 internal。应该强制调用方调用封装非托管代码的托管包装方法。

防范缓冲区溢出了吗?

非托管代码容易受到缓冲区溢出之类的输入攻击。非托管代码 API 应该检查所提供的参数的类型和长度。但是,您不能依赖这一点,因为您可能不拥有非托管源代码。所以,托管包装代码必须严格地检查输入和输出参数。有关更多信息,请参阅本单元中的“缓冲区溢出”。

所有适用于 C 和 C++ 的代码审查规则和规范也适用于非托管代码。

对枚举类型进行范围检查吗?

在将它们传递给本机方法之前应该验证所有枚举值是否都在范围内。

非托管代码方法使用命名约定了吗?

所有非托管代码都应该用以下名称的包装类封装:NativeMethodsUnsafeNativeMethodsSafeNativeMethods。您必须彻底审查 UnsafeNativeMethods 中的所有代码以及传给本机 API 的参数是否存在安全缺陷。

调用了有可能存在危险的 API 了吗?

您应能证明所有 Win32 API 调用的使用的合理性。危险的 API 包括:

切换安全上下文的线程处理函数

访问标记函数,它可以更改和泄漏有关安全标记的信息

凭据管理函数,包括创建标记的函数

Crypto API 函数,它们可以解密和访问私钥

内存管理函数,可以读写内存

LSA 函数,可以访问系统机密

ASP.NET 页和控件

使用本部分中的审查问题审查您的 ASP.NET 页和控件。有关本部分中所提出问题的更多信息,请参阅“构建安全的ASP.NET 页和控件”单元。

禁用详细错误消息了吗?

禁用跟踪了吗?

验证窗体字段输入了吗?

容易受到 XSS 攻击吗?

验证查询字符串和 cookie 输入了吗?

依赖 HTTP 头的安全性了吗?

保护查看状态了吗?

防止 XSS 了吗?

您的 global.asax 事件处理程序安全吗?

提供适当授权了吗?

禁用详细错误消息了吗?

如果您允许异常传播到应用程序边界之外,ASP.NET 可返回详细的信息给调用方。这包括完整的堆栈跟踪和其他对攻击者有用的信息。检查 <customErrors> 元素并确保模式属性设置为“On”或者“RemoteOnly”。

<customErrors mode="On" defaultRedirect="YourErrorPage.htm" />

禁用跟踪了吗?

跟踪信息也对攻击者极为有用。检查 <trace> 元素,确保跟踪被禁用。

<trace enabled="false" localOnly="true" pageOutput="false" 
       requestLimit="10" traceMode="SortByTime"/>

验证窗体字段输入了吗?

攻击者可通过公布的窗体字段向 Web 页和控件传递恶意输入。应该检查是否验证了所有窗体字段输入,包括隐藏的窗体字段。验证它们的类型、范围、格式和长度。使用以下问题审查 ASP.NET 输入的处理:

您的输入包括文件名或者文件路径吗?

通常您应该避免这样做,因为这是一种高风险的操作。为什么需要用户指定文件名或者路径而不是让应用程序根据用户标识选择位置呢? 如果您接受文件名和路径作为输入,代码将非常容易出现规范化错误。如果您必须从用户接受路径输入,则应该检查是否验证了路径安全性和规范化。检查代码是否使用了 System.IO.Path.GetFullPath

调用 MapPath 了吗?

如果您通过用户提供的文件名调用 MapPath,应该检查您的代码是否使用了接受一个 bool 参数的 HttpRequest.MapPath 改写,这可防止跨应用程序的映射。

try
{
  string mappedPath = Request.MapPath( inputPath.Text, 
                                       Request.ApplicationPath, false);
}
catch (HttpException)
{
  // Cross application mapping attempted.
}
  

有关更多信息,请参阅“构建安全的 ASP.NET 页和控件”单元中的“使用 MapPath”部分。

如何验证数据类型?

检查您的代码是否验证了从公开的窗体字段接收的数据和其他形式的 Web 输入(如查询字符串)的数据类型。对于非字符串数据,应该检查代码是否通过使用 .NET Framework 的类型系统执行了类型检查。您可以将字符串输入转换为一个强类型的对象,并捕获任何类型转换异常。例如,如果一个字段包含日期,应该使用它构造一个 System.DateTime 对象。如果它包含以年表示的年龄,通过使用 Int32.Parse 将其转换为 System.Int32 对象,并捕获格式异常。

如何验证字符串类型?

检查输入字符串是否通过使用正则表达式进行了长度验证、可接受的字符集和模式验证。您可以使用一个 RegularExpressionValidator 验证控件或者直接使用 RegEx 类。不要搜索无效数据;只搜索已知为正确的信息格式。

使用验证控件了吗?

如果您使用验证控件,如 RegularExpressionValidator、RequiredFieldValidatorCompareValidatorRangeValidator 或者 CustomValidator,检查您是否没有禁用服务器端验证和并不完全依赖客户端进行验证。

依赖客户端验证吗?

不要这样做。客户端验证只能用来改善用户体验。检查是否所有输入都在服务器上进行了验证。

是否容易受到 XSS 攻击?

一定要审查 Web 页是否存在 XSS 缺陷。有关更多信息,请参阅本单元前面的“跨站点脚本攻击 (XSS)”。

验证查询字符串和 cookie 输入了吗?

检查您的代码是否验证了 URL 查询字符串传递的输入字段和从 cookie 提取的输入字段。为了定位有问题的代码,搜索以下文本字符串:

“Request.QueryString”

“Request.Cookies”

检查输入是否使用类型化对象和正则表达式验证了类型、范围、格式和长度,像对窗体字段那样(请参阅前面的部分“验证窗体字段输入了吗?”)。还要考虑对从用户输入派生的任何输出进行编码的 HTML 或者 URL,因为这将拒绝任何可能导致 XSS 错误的无效构造。

保护查看状态了吗?

如果您的应用程序使用查看状态,它是防篡改的吗?审查下列问题:

查看状态保护在应用程序级启用了吗?

检查应用程序 Machine.config 或者 Web.config 文件中的 <pages> 元素的 enableViewState 属性,检查查看状态是否在应用程序级启用。然后检查 enableViewStateMac 是否设置为 "true",以确保它是防篡改的。

<pages enableViewState="true" enableViewStateMac="true" />
  

按页改写查看状态保护了吗?

检查在 Web 页顶端的页级指令,验证是否启用了此页的查看状态。查找 enableViewStateMac 设置,如果有则检查它是否设置为 true。如果没有 enableViewStateMac,或者未设置为 true,页将采用 Web.config 文件中指定的应用程序级默认设置。如果您已经通过将enableViewState 设置为“false”而禁用了页的查看状态,则保护设置将没有关系。

改写代码中的查看状态保护了吗?

检查您的代码是否没有通过设置 Page.EnableViewStateMac 属性为 false 而禁用查看状态保护。这只在页不使用查看状态时是一种安全设置。

您的 global.asax 事件处理程序安全吗?

global.asax 文件包含 ASP.NET 和 HTTP 模块生成的应用程序级事件的事件处理代码。审查以下事件处理程序以确保代码中不包含缺陷:

Application_Start。此处的代码运行在 ASP.NET 进程帐户的安全上下文下,而不是模拟用户。

Application_BeginRequest。此处的代码运行在 ASP.NET 进程帐户的安全上下文下,或模拟用户下。

Application_EndRequest。如果您需要修改外出 cookie 的属性,例如设置“Secure”位或者域,Application_EndRequest 是执行这些操作的恰当位置。

Application_AuthenticateRequest。这将执行用户身份验证。

Application_Error。当调用这个事件处理程序时安全上下文可能对写 Windows 事件日志产生影响。安全上下文可能是进程帐户或者模拟帐户。

protected void Session_End。这个事件的激发是非确定性的,而且只适用于进程内会话状态模式。

提供了适当授权吗?

审查以下问题以验证授权方法:

划分受限的和公开访问区域之间的 Web 站点了吗?

如果您的 Web 应用程序要求用户在可访问特定页之前完成身份验证,应该检查受限页是否置于与可公开访问页不同的目录中。这使您可配置受限的目录以要求 SSL。这还有助于确保身份验证 cookie 不会使用 HTTP 通过未加密的会话传递。

如何保护对受限页的访问?

如果您使用 Windows 身份验证,是否配置了页的 NTFS 权限(或者包含受限页的文件夹)只允许授权用户访问?

是否配置了 <authorization> 元素,指定了哪些用户和用户组可访问特定页?

如何保护对页类的访问?

是否在类上使用了附加的主体权限要求,以决定哪些用户和用户组可访问类?

使用 Server.Transfer 了吗?

如果您使用 Server.Transfer 将用户转到另一个页,应该确保当前已经过身份验证的用户得到授权访问目标页。如果您使用 Server.Transfer 转向用户没有授权查看的页,页仍然要处理。

Server.Transfer 使用不同的模块处理页,而不是从服务器发出另一个请求,这将强制授权。如果需要关注目标 Web 页的安全问题,请不要使用 Server.Transfer。改而使用 HttpResponse.Redirect

Web 服务

ASP.NET Web 服务与 ASP.NET Web 应用程序有许多功能是相同的。在解决特定于 Web 服务的以下问题之前,对照“ASP.NET 页和控件”部分中的问题审查 Web 服务。有关本部分中所提出问题的更多信息,请参阅“构建安全的 Web 服务”单元。

公开了受限操作或者数据吗?

如何授权调用方?

限制特权操作了吗?

使用自定义身份验证了吗?

验证所有输入了吗?

验证 SOAP 头了吗?

公开了受限操作或者数据吗?

如果您的 Web 服务公开了受限操作或者数据,应该检查服务是否对调用方进行了身份验证。您可以使用平台身份验证机制如 NTLM、Kerberos、基本身份验证或者客户端 X.509 证书,还可以在 SOAP 头中传递身份验证令牌。

如果传递身份验证令牌,可以通过 Web Services Enhancements (WSE) 以符合新出现的 WS-Security 标准的方式使用 SOAP 头。

如何授权调用方?

选择 .NET Framework 提供的合适授权方案(如 URL 授权、文件授权、.NET 角色)或者平台的选择如文件 ACL。

限制特权操作了吗?

代码访问安全策略的信任级决定了 Web 服务可访问的资源类型。检查 Machine.config 或者 Web.config 中的 <trust> 元素配置。

使用自定义身份验证了吗?

使用 Web Service Enhancements (WSE) 提供的功能,而不是创建自己的身份验证方案。

验证所有输入了吗?

在使用它们或者将它们传递给下游组件或者数据库之前,如果输入是从当前信任边界之外的来源接收的,检查所有公开的 Web 方法是否验证了其输入参数。

验证 SOAP 头了吗?

如果您在应用程序中使用自定义 SOAP 头,应该检查信息是否没有篡改或者重放。对头信息进行数字签名以确保它没有篡改。您可以使用 WSE 辅助以标准方式对 Web 服务消息进行签名。

检查是否使用 SoapExceptionSoapHeaderException 对象得体地处理了错误,并只向客户端提供了最少量的必需信息。验证是否为故障排除而适当记录了异常。

服务组件

本部分确定了当您审查企业服务应用程序中使用的服务组件时,应该考虑的关键审查点。有关本部分中所提出问题的更多信息,请参阅“构建安全的服务组件”单元。

使用程序集级元数据了吗?

防止匿名访问了吗?

使用受限的模拟级了吗?

使用基于角色的安全了吗?

使用方法级授权了吗?

使用对象构造函数字符串了吗?

在中间层审核了吗?

使用程序集级元数据了吗?

检查您是否使用了程序集级元数据定义企业服务安全设置。使用 assemblyinfo.cs 文件和属性定义身份验证和授权配置。这有助于确保在管理时正确地建立设置。虽然管理员可改写这些设置,它为管理员提供了您期望设置如何配置的清晰定义。

防止匿名访问了吗?

检查您的代码是否使用 ApplicationAccessControl 属性指定了身份验证级。搜索“AuthenticationOption”字符串以定位相关属性。检查您至少使用了调用级身份验证确保每个对组件的调用都进行身份验证。

 [assembly: ApplicationAccessControl(
                       Authentication = AuthenticationOption.Call)]
  

使用受限的模拟级了吗?

为您的服务组件所定义的模拟级决定了要与之通信的任何远程服务器的模拟能力。搜索“ImpersonationLevel”字符串以检查您的代码是否设置了级别。

 [assembly: ApplicationAccessControl(
              ImpersonationLevel=ImpersonationLevelOption.Identify)]
  

检查您是否设置了远程服务器必需的最受限的级别。例如,如果服务器需要为身份验证目的进行标识,但是不需要模拟,应该使用上述的标识级。在 Windows 2000 上使用委托级模拟应该小心,因为从计算机到计算机传递安全上下文的次数是无限的。Windows Server 2003 引入了受限委托。

在 Windows Server 2003 和 Windows2000 Service Pack 4 和更高版本上,模拟特权并不授予所有用户。

如果您的组件在服务器应用程序中,上面显示的程序集级属性控制了组件向企业服务注册时最初的配置。

如果您的组件是在库应用程序中,客户端进程决定了模拟级。如果客户端是一个 ASP.NET Web 应用程序,检查 Machine.config 文件中是否有 <processModel> 元素的 comImpersonationLevel 设置。

使用基于角色的安全了吗?

通过审查以下问题,检查您的代码是否正确地使用基于角色的安全来防止未授权访问:

启用了基于角色的安全吗?

检查基于角色的安全是否启用。在 Windows 2000 上默认时是禁用的。检查您的代码是否包含以下属性:

 [assembly: ApplicationAccessControl(true)]
  

使用组件级访问检查吗?

如果 COM+ 角色在接口级、组件级或者方法级使用,而且不只是用来限制访问应用程序时,COM+ 角色是最有效的。检查您的代码是否包括以下属性:

 [assembly: ApplicationAccessControl(AccessChecksLevel= 
               AccessChecksLevelOption.ApplicationComponent)]
  

还要检查每个类是否用 ComponentAccessControl 属性批注,如下所示:

 [ComponentAccessControl(true)]
public class YourServicedComponent : ServicedComponent
{
}
  

在代码中执行角色检查吗?

如果您的方法代码调用 ContextUtil.IsCallerInRole,检查这些调用是否在 ContextUtil.IsSecurityEnabled 调用之后。如果安全没有启用,IsCallerInRole 总是返回 true。检查若安全没有启用,您的代码是否返回安全异常。

使用对象构造函数字符串了吗?

搜索代码中的“ConstructionEnabled”以定位使用对象构造字符串的类。

 [ConstructionEnabled(Default="")]
public class YourServicedComponent : ServicedComponent, ISomeInterface
  

如果您使用对象构造函数字符串,审查以下问题:

在构造函数字符串中存储敏感数据了吗?

如果您存储数据如连接字符串,检查数据是否在 COM+ 编录中存储之前进行了加密。您的代码应该在通过 Construct 方法传给组件时解密数据。

提供默认构造字符串了吗?

如果数据比较敏感,就不要这样做。

在中间层审核了吗?

您应该跨分布式应用程序的各层进行审核。检查您的服务组件日志操作和事务。原始调用方标识是通过 SecurityCallContext 对象获得的。只有您的应用程序安全级为进程和组件级检查通过使用以下属性进行配置时,才能获得:

 ; [assembly: ApplicationAccessControl(AccessChecksLevel= 
               AccessChecksLevelOption.ApplicationComponent)]
  

远程处理

本部分确定了当您审查使用 .NET 远程处理的代码时应该考虑的关键审查点。有关本部分中提出的问题的更多信息,请参阅“构建安全的远程组件”单元。

将对象作为参数传递了吗?

使用自定义身份验证和主体对象了吗?

如何配置代理凭据?

将对象作为参数传递了吗?

如果您使用 TcpChannel,而且您的组件 API 接受自定义对象参数,或者如果自定义对象是通过调用上下文传递的,您的代码会有两个安全缺陷。

如果作为参数传递的对象派生自 System.MarshalByRefObject,它是通过引用传递的。在此情况下,对象要求 URL 支持对客户端的回调。客户端 URL 有可能被欺骗,这会引起对另一台计算机回调。

如果作为参数传递的对象支持序列化,对象是通过值传递的。在此情况下,应该检查您的代码是否在服务器上反序列化字段时验证每个字段项,以防止恶意数据的注入。

为了防止自定义对象通过引用或者通过值传递给远程组件,将服务器端格式化程序信道接收器的 TypeFilterLevel 属性设置为 TypeFilterLevel.Low

为了定位传入调用上下文的对象,搜索“ILogicalThreadAffinative”字符串。只有实现了此接口的对象可传入调用上下文。

使用自定义身份验证和主体对象了吗?

如果您使用自定义身份验证,是否依赖从客户端传递而来的主体对象呢?这存在潜在的危险,因为恶意代码可创建一个包含扩展角色的主体对象来提升特权。如果您使用这一方法,应该检查您只在带外机制如 IPSec 策略中使用它,这些机制限制了可连接组件的客户端计算机。

如何配置代理凭据?

审查您的客户端代码是如何在远程代理上配置凭据的。如果使用显式凭据,这些凭据应该保留在哪里呢?它们应该加密并存储在安全的位置如受限的注册表项。它们不应以明文进行硬编码。理想情况下,客户端代码应该使用客户端进程标记和使用默认凭据。

数据访问代码

本部分确定了当您审查数据访问代码时应该考虑的关键审查点。有关本部分中所提出问题的更多信息,请参阅“构建安全的数据访问”单元。

防止 SQL 注入了吗?

使用 Windows 身份验证了吗?

保护数据库连接字符串了吗?

如何限制未授权代码?

如何保护数据库中的敏感数据?

处理 ADO.NET 异常了吗?

关闭数据库连接了吗?

防止 SQL 注入了吗?

检查您的代码是否通过验证输入防止了 SQL 注入攻击,使用最低特权帐户连接数据库,并使用参数化存储过程或者参数化 SQL 命令。有关更多信息,请参阅本单元前面的“SQL 注入”。

使用 Windows 身份验证了吗?

通过使用 Windows 身份验证,就无需跨网络将凭据传递到数据库服务器,您的连接字符串也不用包含用户名和密码了。Windows 身份验证连接字符串使用 Trusted_Connection='Yes' 或者 Integrated Security='SSPI',如下例中所示。

"server='YourServer'; database='YourDatabase' Trusted_Connection='Yes'"
"server='YourServer'; database='YourDatabase' Integrated Security='SSPI'"
  

保护数据库连接字符串了吗?

审查您的代码是否正确和安全地使用了数据库连接字符串。这些字符串不应该以明文硬编码或者存储在配置文件中,尤其在连接字符串包含用户名和密码的时候。

搜索“Connection”字符串以定位 ADO .NET 连接对象的实例,审查如何设置 ConnectionString 属性。

加密连接字符串了吗?

检查代码是否检索然后解密加密的连接字符串。代码应该使用 DPAPI 加密以避免密钥管理问题。

使用空白密码了吗?

不要这样做。检查是否所有 SQL 帐户都有坚固的密码。

使用 sa 帐户或者其他高特权帐户了吗?

不要使用 sa 帐户或者任何高特权帐户,如 sysadmin 或者 db_owner 角色的成员。这是一个常见的错误。检查您是否在数据库中使用了具有受限权限的最低特权帐户。

使用持久安全信息了吗?

检查 Persist Security Info 属性是否没有设置为 true 或者 yes,因为这会允许敏感信息包括用户名和密码在连接打开后从连接获取。

如何限制未授权代码?

如果您编写了一个数据访问类库,如何防止未授权代码通过访问库来访问您的数据库呢?一个方法是使用 StrongNameIdentityPermission 要求限制呼叫代码只能是用特定的强名称私钥签名的代码。

如何保护数据库中的敏感数据?

如果您在数据库中存储敏感数据,如信用卡号码,如何保护这些数据呢?您应该检查它是否通过使用坚固的对称加密算法如 3DES 进行了加密。如果您使用这种方法,如何保护 3DES 加密密钥呢?您的代码应该使用 DPAPI 加密 3DES 加密密钥并将加密密钥存储在一个受限的位置,如注册表。

处理 ADO .NET 异常了吗?

检查所有数据访问代码是否放在 try/catch 块中,而且代码是否处理了 SqlExceptionsOleDbExceptions 或者 OdbcExceptions,这取决于您使用何种 ADO .NET 数据提供程序。

关闭数据库连接了吗?

检查您的代码不会在出现异常等等情况时,出现导致数据库连接没有关闭的缺陷。检查代码是否在 finally 块中关闭连接,或者连接对象是在 C# using 语句中构造的,如下所示。这将自动确保连接关闭。

using ((SqlConnection conn = new SqlConnection(connString)))
{
  conn.Open();
  // Connection will be closed if an exception is generated or if control flow
  // leaves the scope of the using statement normally.
}
  

小结

安全代码审查与常规的代码审查或者检查类似,只不过其中心是识别可能导致安全缺陷的编码缺陷。所带来的好处是,消除安全缺陷经常会使代码更加坚固。

本单元说明了如何审查托管代码中的最大安全问题,包括 XSS、SQL 注入和缓冲区溢出。还说明了如何识别其他更微妙的可能导致安全缺陷和成功攻击的缺陷。

安全代码审查并非万能仙丹。但是,它们可能非常有效,而且应该成为开发生命期中的公认的大事。

其他资源

有关更多信息,请参阅 MSDN 文章,“Secure Coding Guidelines for the .NET Framework,”,网址是:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/seccodeguide.asp

展开阅读全文

没有更多推荐了,返回首页