还为安全问题而苦恼么?使用ASP.NET 2.0吧!

原文出处:SECURITY:Security Headaches? Take ASP.NET 2.0!

  本文依据2004年三月技术社区中关于 ASP.NET 2.0 预览版中的内容所作,正式版中所有与本文相关的内容均可能发生改变。

本文涉及如下技术:ASP.NET,验证

本文所讨论内容:

  • ASP.NET 2.0 中增强的安全性
  • 服务器端安全控件
  • 用户与角色数据库
  • 无Cookie的表单验证
  新的安全特性在ASP.NET2.0中是一项重要的改进。这些特性包括一系列会员服务:管理用来存储用户帐号和散列密码的数据库,管理用户会员角色的角色管理器,以及5个新增的服务器端控件,这使得实现基于表单的验证更加容易。ASP.NET 2.0 还提出了一个供给者模型使你可以完全控制关于会员和角色的服务以及无Cookie表单验证的实现。对于用户帐号和角色的本地与远程管理,你也可以使用一种简便的基于Web的管理方式,同时也增强了对其他不安全设定的控制。

表单验证获得改进
  表单验证是ASP.NET1.0中最受欢迎的特性,因为它封装了一些在许多特殊应用中所缺少的最佳解决方案。例如,你知道有多少表单验证的实现保证了那些存放客户身份信息的Cookie的真实性?表单验证不仅将用户名写入Cookie,它还写入信息验证码(一种来自Cookie和只有服务器知道的密值的散列形式)。这就可以阻止那些恶意用户通过改变Cookie中的名字来提高权限或查看其他用户的数据。
  如果你使用过.NET WEB开发者发表的各种新闻组和列表服务,你会发现人们一次次地重复实现同一件事情:用户数据库、保存角色的Cookie、捕获用户名与密码的控件以及管理用户和角色的工具。ASP.NET小组已经为我们提供了对应于这些问题的集成解决方案 。在研究ASP.NET2.0的alpha版时,我发现仅需要编写少量代码就可以构建一个便于管理的使用表单验证的网站,我被这个强大的功能所征服了。

开始
  通过我为你设计的实验,你会发现使用这些新特性来开始工作是多么的简单,如果你有ASP.NET2.0开发环境(只有订阅了MSDN宇宙版的用户才可以下载),你就可以进行此实验。
  首先,你需要建立一个虚拟目录,它指向一个空目录。你必须确信你的ASP.NET工作进程拥有读取、执行和写入的权限。如果你的操作系统是Windows2000或者WindowsXP,你需要给ASP.NET本地帐户赋予以上权限,而如果你使用的是Windows Server 2003,那么你就需要给网络服务帐号赋予以上权限。
  我将使用表单验证,所以我需要通过Web.Config文件来开启此功能。如果我以前向你演示过如何使用ASP.NET1.1的话,那么我会告诉你打开一个文本编辑器并手工敲入XML文本。但是我最喜欢的ASP.NET2.0的新特性之一就是被集成到IIS的管理控制台中的交互式配置文件编辑器,你可以在虚拟目录属性页上的"ASP.NET"标签上找到它。按下"编辑配置"按钮弹出编辑器。

Figure 1 Configuration Editor
Figure 1 配置编辑器

  Figure 1 展示了这个新的编辑器。你可以看到我选择了表单验证,而不是默认的Windows验证。对你的虚拟目录进行此项操作。当你使用这个配置工具时,设置你的Web应用程序的默认语言为C#,这会节省以后你在配置文件中的文本输入。页面默认语言的设定就在"应用程序"属性页的第一个下拉列表中。应用这些改变后,你会在你的虚拟目录中发现一个Web.config文件,并且你的所有设定已经写好。
  你需要通过会员服务来注册一些用户以便开始运行程序,所以你将要写的第一个页面就是允许添加用户的页面。这里有一个为beta版测试用的服务器控件可以使你仅用三行代码就能实现此页面:
<form runat=''server''>
<asp:createuser runat=''server''/>
</form>

  在我使用alpha版时,我还不得不直接使用会员类手工为这个表单编写代码。现在,只需使用Figure 2 所示的ASPX页,而且我将在下文中介绍会员类。Figure 3 展示了当你将浏览器定位到这一页时所看到的内容。现在继续添加一些用户和密码。你的工作会因此变得如此简单。

Figure 3 Membership Page
Figure 3 会员页

  一旦你添加完用户,请仔细查看虚拟目录。你会看到一个新的名为"DATA"的子目录。其中有一个Access数据库。在这里存放着默认的会员与角色服务信息,后面我会向你介绍如何使用 SQL Server 或你自己定义的数据存储程序来存储这些信息。现在是使用ASP.NET2.0中安全控件的时候了。

服务器端安全控件
  Figure 4 列出了ASP.NET2.0中5个新的安全控件。先来介绍一下登陆状态(LoginStatus)控件。首先在新创建的ASPX页面中放置此控件。为了简便,将此页命名为Default.aspx:

<form runat=''server''>
<asp:loginstatus runat=''server''/>
</form>
  用浏览器打开此页,你会看到一个登陆链接。如果你在浏览器中查看源文件,你会看到此链接指向一个叫做login.aspx的页面,而此页面你还没有写。这就是那个只有三行代码的页面,让我们来创建它:
<form runat=''server''>
<asp:login runat=''server''/>
</form>
  如果你以前曾经手工实现过表单验证,你会认为这三行代码很有价值。在过去,实现相同的数据库查询操作至少需要两倍于此数量级的代码。
  现在回到你的浏览器并点击登录链接,转到如 Figure 5 所示的登录页。尝试输入非法的用户名和密码,你会看到默认的错误信息。这个信息不会给攻击者提供详细的内容。开发者无法将错误消息随意地发给用户告诉他们用户名正确,请尝试其他的密码!

Figure 5 Login Page
Figure 5 登录页

  继续输入正确的用户名和密码——之前通过Adduser.apsx页面添加过的——你会被重新定向到Default.aspx页面。在你没有为登录控件提供自定义行为之前,它会简单地通过表单验证并让你登陆,这意味着你的浏览器已经在Cookie中保存了经过加密的用户名。
  现在你已经被重定向到default.aspx页面,你看到有何不同了么?登陆状态控件将显示退出(Logout)而不是开始的登录(Login)。因为表单的验证Cookie通过Request请求被发送出去,表单验证模块创建了用户通过验证的信息,并与request上下文进行关联。登录状态控件注意到此信息并允许你退出。尝试退出并重新登陆来查看此过程。
  现在让我们为default.aspx页面添加一点代码:
<h3>User Name: <%= User.Identity.Name %>
</h3>
<h3>User Type: <%= User.GetType() %></h3>
  刷新页面,你会看到你登录所使用的用户名。注意下面那个代表用户的对象,它是一种生成规则类型,表单验证模块通过此类型来表现用户。一旦你打开了角色管理器你会看到此类型发生了改变,因为当开启角色管理器后,新的角色管理模块会利用它自己的生成规则来代替表单验证模块的生成规则。
  现在让我们为default.aspx页面添加一个登录视图控件,用以显示其他根据用户登录产生的信息。使用此控件最简单的方法是提供两项内容:一个用于匿名请求(用户登录前),另一个用于验证通过的请求(在用户登录后):
<asp:loginview runat=''server''>
<anonymoustemplate>
<h4>If you see this, you''ve not yet logged in!</h4>
</anonymoustemplate>
<loggedintemplate>
<h4>Welcome to my website, <asp:loginname runat=''server''/>!</h4>
</loggedintemplate>
</asp:loginview>
  当你登录和退出时,你会看到登录视图控件中的文字按照预期的方式改变。这是个非常简单的想法,但确实使你的全部代码变的清晰了。

定义角色
  我已经建立了一个简单的页面,它使你通过角色管理器将用户添加到角色中,但是在你使用它之前需要为你的应用程序开启用户管理器。回到配置工具,并找到验证标签。在”开启角色管理器“复选框前打勾并应用这次改变。
  Addrole.aspx页面的代码在 Figure 6 中,而 Figure 7 展示了表单的外观。将此页放到虚拟目录中并用浏览器打开此页,添加一些角色。指定一个用户名(之前你在adduser.aspx页面中添加的)和一个角色名,并按下按钮将用户添加到角色中。首先,如果这个角色不存在的话,代码会自动将其添加,然后将用户添加到此角色。在后台,角色管理器遍历Access数据库中角色信息在内存中的映射,这个数据库正是会员服务所使用的那个数据库,确实很凑巧。角色管理器可以将数据保存在SQL Server或其他数据存储设备中,并且不需要使用与会员服务相同的机制。这是因为,会员与角色管理器的提供者模型截然不同。

Figure 7 Add Role
Figure 7 添加角色

  如果你曾经在ASP.NET中实现过自定义角色,你会觉得内建的角色管理器十分有价值,你不必为了实现基于角色的安全去管理ASP.NET HTTP管道。一旦你添加了一些角色,你可以回到default.asp页面并轻松地使用登录视图控件。在<loggedintemplate/>元素之后添加另一段:
<rolegroups>
<asp:rolegroup roles=''ForumModerators''>
<contenttemplate>
<h4>Controls for forum moderators
go here.</h4>
</contenttemplate>
</asp:rolegroup>
<asp:rolegroup roles=''Friends''>
<contenttemplate>
<h4>Welcome, friend!</h4>
</contenttemplate>
</asp:rolegroup>
</rolegroups>
  也许你使用的角色与我的不同,所以你需要将我所用的角色替换为你的角色,并调节内容以适应这个角色。完成后,以不同角色的不同帐号登录来测试你的新页面,并监视当角色改变后页面内容有何改变。注意,如果有两个角色组符合用户的角色,将永远显示第一组(从上到下排列)。
  虽然这不是新特性,但是请记住你总是可以通过User.IsInRole属性来以编程的方式测试角色。同时记住你还可以通过web.config文件中的<authorization/>节点来设置准许或否定对各种页面的访问,如下:
<authorization>
<deny users=''?''/>
<allow roles=''ForumModerators''/>
<deny users=''*''/>
</authorization>
  第一个条告诉ASP.NET弹回任何未经验证的请求(强制验证)。第二条和第三条确定只有角色为ForumModerators的用户才可以访问web.config文件所在目录中的内容。记住authorization节点可以在子目录中的web.config文件中使用,也可以在<location/>元素中使用来控制访问单独的文件。

密码恢复
  在demo中我没有向你演示密码恢复控件,因为使用它需要深思熟虑。你也许知道此控件的作用:它可以让用户请求将自己的密码以电子邮件的方式发送给自己。在你决定将用户的密码明文发送给用户前,需要做一下风险评估。
  事实上,如果你仅仅将这个控件放到你的站点页面上,它并不会工作,因为会员服务默认设定为拒绝透露明文密码。它甚至不会按预想的方式工作。因为它默认为保存密码的单向散列值,而不是密码本身。当验证密码时,会员服务将当前密码散列,并与它的拷贝值进行比较。如果你想恢复明文密码,你可以重新配置会员提供者以加密的形式保存密码,这种情况下提供者将使用<machineKey/>加密密码。因此可以解密并发送给用户。
  如果你保存散列后的密码,这确实很好,你需要准备另一套方法来验证用户。你不能给用户发e-mail来告知密码,但如果你在之前提一些问题,例如”你最喜欢的宠物的名字是什么?“,你可以使用答案来验证用户并允许他给你发送一个新密码。然而会员服务不支持为每一个用户保管问题与答案。它只能用于决定是否发送e-mail通知密码。我建议这里可以作些文章。
  Viega 与 McGraw 在《建立安全的软件》(Addison-Wesley, 2002)一书的95页提出了通过问题与答案进行密码重置的较好模型。它需要使用一个由数百个问题组成的集合,当用户第一次创建他的帐号时,可以从中随机选取一些列问题提问用户。当用户提出重置密码请求时,就可以从他回答过的问题中选择一些来提问用户。需要他正确地回答这些问题以便继续下去。如果用户重置密码成功,你需要用一组新的随机选取的问题来代替以前所使用的问题。

提供者调节
  到目前为止,我故意使用默认设置以保持简单性,但是你需要根据你的环境调节这些设定。例如,如果你希望会员服务将数据保存在SQL Server中,你应该选择AspNetSqlProvider而不是AspNetAccessProvider——默认值。这个设定在配置工具对话框的验证页中。
  但是如果你已经有一个现存的用户数据库需要集成会怎样呢?它当然不会有AspNetSqlProvider需要的表和列。此外如果它安装在一个AS/400服务器或是一个Oracle会怎样呢?幸运的是,会员与角色管理都建立在一个分层模型上,如 Figure 8 所示。 你可以通过扩展的MembershipProvider抽象类完全替换会员的数据存储,MembershipProvider定义于System.Web.Security 命名空间。同样,你可以通过扩展RoleProvider类替换角色管理的数据存储。Rob Howard在他的:Nothin'' But ASP.NET 专栏中详细讨论了提供者模型。

Figure 8 Provider Model
Figure 8 提供者模型

  使用现存的提供者无疑是及其简单的。在alpha版中,有两个提供者。一个用于Access数据库,就如你所看到的,它工作的很好。第二个是前面提到的SQL Server提供者。在beta版发布时将会有一个针对活动目录进行用户验证的会员提供者,和一个从授权管理器中查找角色的角色提供者。
  即使你选择了内建的提供者之一,你也可以在web.config中调节它的行为。Figure 9 展示了SQL Server会员提供者的设定。注意密码格式的设定,你可以选择三个选项:散列(默认),加密,以及明文。之后你可以通过enablePasswordRetrieval和requiresQuestionAndAnswer 选择你的密码取回策略。当然,如果你选择使用散列密码,你需要将enablePasswordRetrieval属性设置为false。否则,你需要在系统发送密码电子邮件前让用户回答预先安排好的问题。

Figure 9 Provider Settings
Figure 9 提供者设定

  数据库的连接字符串没有保存在你的web.config文件中;而是间接被引用。注意这个属性叫做连接字符串名并指向一个machine.config的特定节点,它被设计用来保存连接字符串。将连接字符串放在你的web.config文件之外是个好主意,尤其是如果你不能使用集成验证而被迫使用密码验证时。ASP.NET 2.0 预定支持对配置文件中的敏感结点进行XML加密——对于在machine.config中的连接字符串节点来说确实是一个实用的特性。
  角色管理器可被配置为使用cookie或者URL参数并且可以缓存cookie中的角色以减少到角色数据库中取值的次数。缓存十分智能:如果缓存中的角色数变得太大,角色管理器将会只保存最近使用较频繁的角色到缓存中,并动态地查询最近使用过的角色。这个特性很可能是为存储容量有限的移动设备而设计的。
  还有一些其他的设定可以进行调节,但是我会留给你自己去体会。下面让我们来看看如何调节在前面用过的服务器端安全控件。

控件调节
  只用三行代码就创建一个登录页面确实很轻巧,但是一般来说,你需要根据你的程序来自定义登录控件的一些特性。Figure 10 列出了一些可以替换前面那个简单登录页面的代码。另外,可以通过你所熟悉的Web控件属性来改变这些控件的外观。并且在ASP.NET2.0中通过主题支持你可以不用改变代码就使全部网站的页面保持一致的外观。
  登录控件的一个有趣特性是它不必始终在页面上显示着,就像我在本例中所做的。相反,你也可以使它成为你主页的一部分,这样它就永远显示在页边的空白部分。一旦用户登录成功,便不再想看到它了,所以当它检测到用户已经成功通过验证,它就按照默认设置变为不可见。你可以通过VisibleWhenLoggedIn属性来调节此特性。这是ASP.NET开发者们手工实现的特性例子之一,而现在已经内建在ASP.NET2.0中。
  其他控件也有相似的选项。例如,如果你希望为用户显示一个漂亮的登陆按钮,可以通过设置登录状态控件的Login(Out)ImageUrl属性来实现。
  为了获得这些控件工作的整体感,可以使用Visual Studio 2005工程向导来创建网站。只有将"Web.vssettings"IDE配置文件导入到Visual Studio中,向导窗体才会出现。你可以通过导入/导出工具对话框来导入此文件。向导程序包括了目前为止所有我谈到的特性,并且允许进行大量的用户界面设置以以满足你所期望的网站外观与功能。

设计会员与角色
  当你需要更深如地了解服务器端安全控件时,知道直接使用一些类来实现高级功能是有益的。有两个主要的类需要了解以学习这些服务的设计模型:会员类于角色类。我没有足够的篇幅对他们进行全面而详尽的介绍,并且一些细节性的内容将在最终版发布时有所改变,但是我们可以先了解一下。
  通过会员类,你可以创建用户并管理他们,每一个用户都由一个会员类的实例所代表。这个类代表了一个用户的简介,包括的属性有电子邮件,创建日期,密码问题等等。当你创建并更新这些用户简介时,你就可以通过会员类(取决于它的层次模型)来隐藏何时何地存储简介的细节 (见 Figure 8)。这个类还提供了改变用户密码和重置密码为由计算机生成的随机密码的方法,还可以作为一个时间戳跟踪活动的用户以维护当前的用户数量(你可以通过调用会员类的GetNumberOfUsersOnline 函数获得这个数字)。
  要验证用户密码,只需调用会员类的ValidateUser 方法,并将用户名和密码作为参数传入。底层的提供者会处理好必要的密码散列或解密。如果一个用户忘记了他的用户名,你可以通过询问他的电子邮件地址作为参数传递给GetUserNameByEmail方法获得这个用户名并告知用户,但是这并不安全。

无Cookie的表单验证
  在我教授ASP.NET表单验证的课程时,我听到很多人抱怨需要Cookie实现。幸运的是,在ASP.NET2.0中这个限制将不复存在。在web.config文件中的<forms/>元素中有一个新的"cookieless"属性。你可以设定以下四个值其中之一:UseCookies, UseUri, UseDeviceProfile, 或AutoDetect。
  UseCookies和UseUri强迫表单验证模块使用Cookie或者URL参数来处理所有的请求。UseDeviceProfile意味着需要依据浏览器的容量来决定使用哪种模式。最后,AutoDetect会尝试设定一个cookie而如果失败将会使用URL参数来处理。下面有一个典型的在URL中使用参数的例子(圆括号中的参数会使它看起来很长):http://www.acme.com/foo/(F(Cvc...A1))/default.aspx
  URL中的括号部分包含了一般在Cookie中保存的数据,并且被HTTP管道协议中的一个模块所剥离,所以如果你从ASPX页面中 查看Request.Path属性,你不会在URL中看到额外的附着信息。如果你重定向这个请求,URL将被自动加上参数。换句话说,下面的代码会通过使用参数的URL将你带回到你正在查看的相同页面:
Response.Redirect(Request.Path)
  这一特性将使表单验证获得可观的广泛应用。然而,当越来越多的网站使用ASP.NET表单验证时,也会有越来越多的攻击者尝试着发现弱点,所以遵守一些既定规则是明智的。

一些预防措施
  如果不用安全Sockets层(SSL)对表单验证进行保护,它的作用将不会很强。至少,在登录页面被发送到客户端并返回到服务端的过程中,需要通过一个安全的连接防止窃听者嗅探到用户的明文密码。但通常那是不够的。窃听者一旦偷取到了表单验证Cookie他也就偷取到了登录信息,因为根据Cookie的工作方式,无法进行重复检查。记住Cookie一般随着每次请求被发送出去,甚至像请求按钮的Gif图片这样简单的事情。一旦偷取到Cookie,攻击者会使用cookie去伪装成真正的用户。为了减小此风险你需要大幅度地缩短cookie超时时间或者使全部站点(最好还有其他辅助性资源)运行在SSL上。
  当站点需要较高的安全级别时我更倾向于第二种方法,并且当人们抱怨使用SSL如此之慢时我会反问他们为什么不购买更好的硬件加速它的执行。然而,一些公司坚持网站的一部分使用。如果你处于这种情况下,你可以在<forms/>元素中打开requireSSL 属性。这将为表单验证Cookie添加"Secure"属性,它意味着浏览器仅通过一个安全通道发送Cookie到服务器。换句话说,如果不运行在SSL上,请求将不会被发送。这个特性是在.NET框架1.1版本中新增的,所以在ASP.NET2.0中已经不新鲜了。ASP.NET2.0中的新特性是这种防范方法可以应用于保存在Session中的内容:
<httpCookies requireSSL=''true'' httpOnlyCookies=''true''/>
  如果不使用SSL,安全的Cookie就不会被发送,既然这样,对于那些通过原始HTTP协议访问的页面,可以保证User.Identity.IsAuthenticated属性每次都返回false。换句话说,你不清楚哪些用户正在访问没有使用SSL的页面。请注意即使你决定使你的全部网站应用SSL,打开requireSSL也是个不错的主意,以防你偶尔地允许通过原始HTTP协议访问一两个文件。
  httpOnlyCookies属性对付交叉站点脚本攻击十分有用;它意味着浏览器将无法通过脚本语言访问Cookie。它使用了Cookie的一个叫做HttpOnly的属性,这个属性目前只能被新版本的IE浏览器识别,但是我认为其他浏览器厂商会很快跟进。了解更多信息请看: Some Bad News and Some Good News.

结论
  ASP.NET 2.0为采用表单验证的网站提供了重要的安全特性。通过提供一个支持角色的保存用户信息的容器,表单验证将会超越ASP.NET内部的极限而被广泛采用。我将不会再使用以前的技术,因为那令我难以忍受!

Keith Brown 是专攻应用程序安全方面的独立咨询顾问。他定期在 DevelopMentor 讲授有关安全方面的课程。著有:《Programming Windows Security》(Addison Wesley, 2000);目前他正在为 .NET 程序员编写一本关于安全方面的新书。在线阅读这本新书请访问 http://www.develop.com/kbrown。

本文出自 MSDN MagazineApril 2004 期刊,可通过当地 报摊获得,或者最好是 订阅

本文由 VCKBASE MTT 翻译
posted @ 2006-03-31 15:46 torome 阅读(...) 评论(...) 编辑 收藏