Jun
1
说明:本文中所有程序均在Windows 2000 Server中文版 + SP2上编译运行无误
开发环境:.Net 框架1.0 Version 1.0.3705
一、ASP.NET虚拟主机存在的重大隐患
我曾经在WWW.BRINKSTER.COM申请了一个免费的ASP.NET空间,上传了两个程序,其中一个查看目录和文件的程序证明我的判断:ASP共享空间服务器存在的一个安全问题,在 ASP+ 共享空间服务器中依然存在并且变得更加难以防范!通过这个程序我可以浏览所有用户的ASP+程序,可以查看服务器的系统日志……,当然,如果我想删除什么的话也不会有什么问题。为了让大家更清楚地了解这一问题,我们有必要简单介绍一下ASP中就已经存在的这一问题。
ASP中常用的标准组件:FileSystemObject,这个组件为 ASP 提供了强大的文件系统访问能力,可以对服务器硬盘上的任何有权限的目录和文件进行读写、删除、改名等操作。FSO对象来自微软提供的脚本运行库scrrun.dll中。
使用下面的代码就可以在ASP中创建一个FSO对象:
Set fso = CreateObject("Scripting.FileSystemObject")
我们使用fso对象包含的属性和方法,如Drive、Drives、Folder、Floders、File、Files等对服务器的磁盘、目录和文件进行读、写、删除等操作。这一强大的文件系统访问能力给ASP共享空间提供者带来了严重的安全问题,很多ASP空间的管理员都删除此组件或将这个组件改名以避免用户使用这一标准组件。删除组件或组件改名确实是一个简单的方法并且也很有效,但是却使广大用户无法使用它的强大的功能。网络上还有一种看起来很美的方案,它允许用户使用 FileSystemObject 组件又不影响服务器的安全,即对每一个用户都设置一个独立的服务器用户和单个目录的操作权限。但是这种方法是有问题的。因为ASP和ASP.NET中在这方面的问题十分类似,所以我们将在ASP.NET的相应解决办法部分详加说明。
在ASP.NET中我们发现这一问题仍然存在,并且变得更加难以解决。这是因为.NET中关于系统IO操作的功能变得更加强大,而使这一问题更严重的是ASP.NET所具有的一项新功能,这就组件不需要象ASP那样必须要使用regsvr32来注册了,只需将Dll类库文件上传到bin目录下就可以直接使用了。这一功能确实给开发ASP.NET带来了很大的方便,但是却使我们在ASP中将此dll删除或者改名的解决方法失去效用了,防范此问题就变得更加复杂。在讨论解决方案之前,我们先来看一下怎么来实现上述的危险的功能。
二、文件系统操作示例
在我们编写代码之前,有必要了解一下我们需要用到的几个主要的类。这几个类都在System.IO名称空间下,System.IO 名称空间包含允许在数据流和文件上进行同步和异步读写的类。
在整个应用程序的开始部分我们需要了解一下服务器的系统信息,这就需要用到System.Environment类,该类提供有关当前环境和平台的信息以及操作它们的方法。我们通过System.Environment类可以得到系统的当前目录和系统目录,这可以使我们更快的发现几个关键的目录;我们还可以通过获取运行当前进程的用户名来帮助我们了解ASP.NET程序运行所使用的用户,进一步设置用户权限以避免这一安全问题。
我们还要使用System.IO名称空间的其他几个类是:
System.IO.Directory:提供用于创建、移动和枚举通过目录和子目录的静态方法的类
System.IO.File:提供用于创建、复制、删除、移动和打开文件的静态方法的类
System.IO.FileInfo:提供创建、复制、删除、移动和打开文件的实例方法的类
System.IO.StreamReader:实现一个 TextReader,使其以一种特定的编码从字节流中读取字符。
每个我们所使用的类的属性和方法的具体用法我们将以代码注释的方式在程序中加以说明。
System.IO名称空间在 .NET FRAMEWORK提供的mscorlib.dll中,在使用VS.Net编程之前需要将此Dll引用到此项目中。
我们所编写的程序都使用了Codebehind方式,即每一个aspx程序都有一个对应的aspx.cs程序,aspx程序中只是写与页面显示相关的代码,所有逻辑实现的代码都放在相应的aspx.cs文件中,这样就可以更好得做到显示与逻辑的分离。由于我们的目的不是讨论Codebehind技术,所以就不在对此多加讨论了。
在这篇文章里,我们只介绍几个主要的类及其关键方法的用法,详细程序请查看附带的源代码。
程序一:显示服务器的当前信息和全部逻辑驱动器的名称的程序listdrivers.aspx
主要方法1:我们使用 GetSysInf() 方法来得到服务器的当前环境和平台的信息
//获取系统信息的方法,此方法在listdrivers.aspx.cs文件中public void GetSysInf () {//获取操作系统类型qDrives = Environment.OSVersion.ToString();//获取系统文件夹qSystemDir = Environment.SystemDirectory.ToString();/*获取映射到进程上下文的物理内存量,通过这一内存映射量可以了解ASP.NET程序在运行时需要多少系统物理内存,有助于更好的规划我们的整个应用,因为物理内存量是以Byte为单位的,所以我们将此数值除以1024,可以得到单位为KB的物理内存量*/qMo = (Environment.WorkingSet/1024).ToString();//获取当前目录(即该进程从中启动的目录)的完全限定路径qCurDir = Environment.CurrentDirectory.ToString();//获取主机的网络域名qDomName = Environment.UserDomainName.ToString();//获取系统启动后经过的毫秒数qTick = Environment.TickCount;
//计算得到系统启动后经过的分钟数qTick /= 60000;//获取机器名qMachine = Environment.MachineName;//获取运行当前进程的用户名qUser = Environment.UserName;/*检索此计算机上格式为"<驱动器号>:"的逻辑驱动器的名称,返回字符串数组,这是下一步操作的关键所在*/achDrives = Directory.GetLogicalDrives();//获取此字符串数组的维数,确定有多少个逻辑驱动器nNumOfDrives = achDrives.Length;}
系统信息不需要进行操作,我们简单的用asp:Label将他们显示出来就行了。逻辑驱动器的个数在不同的服务器上是不定的,所以用不定长数组保存逻辑驱动器的名称,而且逻辑驱动器的名称也是我们下一步浏览目录和文件的基础,故我们采用了数据网格DataGrid来显示和处理它。
显示和处理逻辑驱动器名称的DataGrid的代码(代码在listdrivers.aspx文件):
<asp:DataGrid id="DriversGrid" runat="server" AutoGenerateColumns="false"><Columns><asp:BoundColumn HeaderText="ID" DataField="ID" /><asp:BoundColumn HeaderText="磁盘名" DataField="Drivers" /><asp:HyperLinkColumnHeaderText="详细信息" DataNavigateUrlField="Drivers" DataNavigateUrlFormatString="listdir.aspx?dir={0}" DataTextField="Detail" Target="_new" /></Columns></asp:DataGrid>
前两个BoundColumn列都是显示序号和实际逻辑驱动器名称的,需要说明的是第三列,我们在进入各个逻辑驱动器显示目录和文件之前需要将所选择的逻辑驱动器的名称传递到显示目录的文件去,所以需要一个特殊的超级链接行HyperLinkColumn,我们将DataNavigateUrlField设置为数据源中要绑定到 HyperLinkColumn 中的超级链接的 URL 的字段,在此即逻辑驱动器名称。然后将DataNavigateUrlFormatString设置为当 URL 数据绑定到数据源中的字段时,此HyperLinkColumn中的超级链接的 URL 的显示格式,即要链接到的下一级处理页面,在此为listdir.aspx?dir={用户点击行的逻辑驱动器名称}
创建数据源的代码(代码在listdrivers.aspx.cs文件中):
//通过此方法返回一个集合形式的数据视图DataViewICollection CreateDataSource() {//定义内存中的数据表DataTableDataTable dt = new DataTable();//定义DataTable中的一行数据DataRowDataRow dr;/*向DataTable中增加一个列,格式:DataColumn("Column", type)Column为数据列的名字,type为数据列的数据类型*/dt.Columns.Add(new DataColumn("ID", typeof(Int32)));dt.Columns.Add(new DataColumn("drivers", typeof(string)));dt.Columns.Add(new DataColumn("detail", typeof(string)));//使用for循环将逻辑驱动器的名称以行的形式添加到数据表DataTable中for (int i = 0; i < nNumOfDrives; i++) {//定义新行dr = dt.NewRow();//对行中每列进行赋值,注意要与上边定义的DataTable的行相对应dr[0] = i; //循环生成的序号dr[1] = achDrives.ToString(); //逻辑驱动器的名称dr[2] = "查看详情";//向DataTable中添加行dt.Rows.Add(dr);}//根据得到的DataTable生成自定义视图DataViewDataView dv = new DataView(dt);//返回得到的视图DataViewreturn dv;}
我们通过这个方法得到了一个包含所有我们需要的数据的数据视图DataView,我们只需要在此aspx页的Page_Load方法中将此数据视图绑定到DataGrid上就可以了。
数据绑定代码(代码在listdrivers.aspx.cs文件中):
/* 设置DataGrid的数据源DataSource为我们从CreateDataSource()方法得到的数据视图DataView */DriversGrid.DataSource = CreateDataSource();//将此DataGrid进行数据绑定DriversGrid.DataBind();
通过上边介绍的几种主要方法我们就实现了获取系统信息和显示所有逻辑驱动器名称的功能,并且可以通过相应的链接进入下一个显示目录和文件名的程序listdir.aspx显示该逻辑驱动器下的所有目录和文件。
程序二:显示目录中所有子目录和文件的程序listdir.aspx
目录下有子目录和文件两种形式,必须分别对待。我们调用此程序本身对子目录进行列表显示,而文件我们需要调用showfile.aspx程序对文件的属性和内容进行显示。并且两者还有不同的删除方法,所以我们在这里设置了两个DataGrid,两个DataTable,两个DataView,分别处理和显示目录和文件。
显示和处理目录和文件的DataGrid的代码(代码在listdir.aspx文件):
显示目录或文件的序号和名称的数据列类似于listdrivers.aspx程序中的相应代码,这里就不再重复了。对于子目录和文件分别有各自的处理页面,所以需要导航到两个不同的页面,对于子目录,我们继续使用listdir.aspx程序对其下的子目录和文件进行列表显示:
<asp:HyperLinkColumn DataNavigateUrlField="DirName"DataNavigateUrlFormatString="listdir.aspx?dir={0}"DataTextField="DirDetail" HeaderText="详细信息"Target="_new"/>
对于文件,我们使用showfile.aspx程序显示其属性和内容:
<asp:HyperLinkColumn DataNavigateUrlField="FileName"DataNavigateUrlFormatString="showfile.aspx?file={0}" DataTextField="FileDetail" HeaderText="详细信息" Target="_new" />
在两个DataGrid(DirGrid,FileGrid)中我们分别设置了两个HyperLinkColumn列来导航到不同的处理页面。
在两个DataGrid中我们都使用了一个删除的按钮列:
<asp:ButtonColumn HeaderText="删除" Text="删除" CommandName="Delete"/>
由于添加、更新、删除功能列都是DataGrid的默认模板列,所以可以在Vs.net中通过DataGrid的属性生成器自动添加此列。
获取上一页面所传递来的参数的代码:
因为在下面产生数据源的方法中需要使用由上一个页面传递过来的参数来确定目录和文件的名称,所以在页面的Page_Load方法里使用了下列代码:
strDir2List = Request.QueryString["dir"];
字符串strDir2List即传过来的目录名或文件名。
因为我们使用了两个DateGrid,就需要进行两次数据绑定,就有两个不同的生成数据源的方法。
生成目录数据网格(DirGrid)数据源的方法:
//通过此方法返回一个集合形式的数据视图DataView,用来初始化子目录的DataGridICollection CreateDataSourceDir() {dtDir = new DataTable();DataRow dr;//向DataTable中添加新的数据列,共四列dtDir.Columns.Add(new DataColumn("DirID", typeof(Int32)));dtDir.Columns.Add(new DataColumn("DirName", typeof(string)));dtDir.Columns.Add(new DataColumn("DelDir", typeof(string)));dtDir.Columns.Add(new DataColumn("DirDetail", typeof(string))); //根据传入的参数(目录名)得到此目录下所有子目录名的字符串数组string [] DirEntries = Directory.GetDirectories(strDir2List);//使用foreach循环可以对未知长度的数组进行遍历循环foreach(string DirName in DirEntries){dr = dtDir.NewRow();dr[0] = i;//序号dr[1] = DirName;//文件夹名称 dr[3] = "删除";dr[3] = "查看详情";dtDir.Rows.Add(dr);i++; }DataView dvDir = new DataView(dtDir);//返回得到的数据视图return dvDir;}生成文件数据网格(FileGrid)数据源的方法://通过此方法返回一个集合形式的数据视图DataView,用来初始化文件的DataGridICollection CreateDataSourceFile() {dtFile = new DataTable();DataRow dr;dtFile.Columns.Add(new DataColumn("FileID", typeof(Int32)));dtFile.Columns.Add(new DataColumn("FileName", typeof(string)));dtFile.Columns.Add(new DataColumn("DelFile", typeof(string)));dtFile.Columns.Add(new DataColumn("FileDetail", typeof(string))); //根据传入的参数(目录名)得到此目录下所有文件名的字符串数组string [] FileEntries = Directory.GetFiles(strDir2List);foreach(string FileName in FileEntries){dr = dtFile.NewRow();dr[0] = i;dr[1] = FileName; dr[2] = "删除";dr[3] = "查看详情";dtFile.Rows.Add(dr);i++;}dvFile = new DataView(dtFile);return dvFile;}
我们编程实现了两个DataSource只需在页面的Page_Load方法里对两个DataGrid进行数据绑定即可将得到的DataTable中的数据显示在aspx页面的DataGrid上。
数据绑定代码:
//对子目录数据列表DirGrid进行数据源定义和数据绑定DirGrid.DataSource = CreateDataSourceDir();DirGrid.DataBind(); //对文件数据列表FileGrid进行数据源定义和数据绑定FileGrid.DataSource = CreateDataSourceFile();FileGrid.DataBind();
通过我们上边介绍的主要方法,我们实现了对某个逻辑驱动器或目录中的所有子目录和文件进行了列表显示,并且可以根据显示结果更进一步的浏览子目录或者查看文件的属性和内容提要。浏览子目录仍然是通过listdir.aspx这个程序,没有任何子目录级别要求,没有目录深度限制。
删除子目录和文件的主要方法和代码:
在删除子目录时,我们需要用到Directory.Delete (string,bool)方法,此方法有两种:
1.public static void Delete(string);
从指定路径删除空目录。
2.public static void Delete(string, boolean);
删除指定的目录并(如果指示)删除该目录中的任何子目录,将boolean设置为true的话,则删除此目录下的所有子目录和文件,否则将boolean设置为false。
在这里我们使用了第二种方法,如果选择删除的话,将删除此目录下的所有子目录和文件。
注意:Directory 类的所有方法都是静态的,因而无需具有目录Directory的实例就可被调用。
/*实现删除子目录的方法,此方法为VS.NET自动添加,注意DataGridCommandEventArgs e为DirGrid中 CommandName="Delete" 的ButtonColumn的事件,通过此事件,我们可以得到是那一行的ButtonColumn按钮列被点击,进而确定我们需要删除的子目录的名称*/private void DirGrid_DeleteCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e){/*定义一个单元格,e.Item为此事件所发生行的所有项目,e.Item.Cells[1]为整个行的第二个单元格的内容,在此DataGrid中为子目录的名称*/TableCell ItemCell = e.Item.Cells[1];//得到此子目录的名称的字符串string item = ItemCell.Text;//删除此子目录Directory.Delete(item,true);//删除后进行数据绑定以更新数据列表DirGrid.DataBind();}
在删除文件时,我们需要用到File.Delete(string path);
注意:File 类的所有方法都是静态的,因而无需具有目录的实例就可被调用。
private void FileGrid_DeleteCommand(object source,System.Web.UI.WebControls.DataGridCommandEventArgs e) {TableCell ItemCell = e.Item.Cells[1];//得到此文件名称的字符串string item = ItemCell.Text;//删除此文件File.Delete(item);//删除后进行数据绑定以更新数据列表DirGrid.DataBind();}
通过上边的主要方法我们在页面上实现了一个删除某一个子目录或者文件的功能,此功能在测试时需要慎重使用,一旦删除无法通过常规方法恢复。其他如目录或文件改名、修改内容等方法都可以在此程序基础上添加相应的功能,实现方法也很简单。各位爱好者可以通过添加相应功能,使之扩充为一个基于Web的服务器文件管理系统。我们也可以由此看到这个程序的危害性,一个没有对此安全隐患采取防范措施的服务器的文件系统就都暴露在了使用此程序的用户面前。
程序三:显示文件属性和内容的程序showfile.aspx
在显示属性和内容时需要用到的两个主要的类:
System.IO.FileInfo:提供创建、复制、删除、移动和打开文件的实例方法,并且帮助创建 FileStream 对象。
System.IO.StreamReader:实现一个 TextReader,使其以一种特定的编码从字节流中读取字符。除非另外指定,StreamReader的默认编码为 UTF-8,而不是当前系统的 ANSI 代码页。UTF-8 可以正确处理 Unicode 字符并在操作系统的本地化版本上提供一致的结果。
Showfile.aspx页面主要代码:
<asp:Label id="FileDetail" runat="server"/>
我们只是将文件的属性信息和部分内容显示在此Label上。所以没有其他复杂的代码。
获取文件信息和内容的主要代码都在Page_Load方法中(代码在showfile.aspx.cs文件中):
//接收传入的参数,确定需要操作的文件名称strFile2Show = Request.QueryString["file"];//根据文件名实例化一个FileInfo对象FileInfo fi = new FileInfo(strFile2Show);FileDetail.Text = "文件名:";FileDetail.Text += strFile2Show+"<br>";FileDetail.Text += "文件大小";//获得文件的大小,然后变换单位为KBFileDetail.Text += (fi.Length/1024).ToString()+"K<br>";FileDetail.Text += "创建文件时间:";//获得文件的创建日期FileDetail.Text += fi.CreationTime.ToString();FileDetail.Text += "上次访问时间:";//获得文件的上次访问日期FileDetail.Text += fi.LastAccessTime.ToString()+"<br>";FileDetail.Text += "上次写入时间:";//获得文件的上次写入日期FileDetail.Text += fi.LastWriteTime.ToString()+"<br>";//实例化一个StreamReader对象,用于读取此FileInfo的内容StreamReader FileReader = fi.OpenText();//定义一个长度为1000的字符数组作为缓冲区char[] theBuffer = new char[1000];/*ReadBlock方法:从当前流中读取最大数量的字符并从索引开始将该数据写入缓冲区。参数:char[] buffer:方法返回时,包含指定的字符数组int index:buffer 中开始写入的位置int count:最多读取的字符数*/int nRead = FileReader.ReadBlock(theBuffer,0,1000);FileDetail.Text += new String(theBuffer,0,nRead);//关闭此 StreamReader 并释放与之关联的所有系统资源FileReader.Close();
到目前为止,我们实现了一个简单的web页面的服务器磁盘管理应用程序,可以查看、删除目录和文件。如果需要修改文件、新建文件和文件夹等功能,只需稍作修改,添加上相应的代码就可以。由于我们只是通过这个程序说明服务器中存在的安全隐患,所以在这里就不再实现这些功能了。
通过这三个简单的程序,我想大家已经能够清楚的认识到这一漏洞的危害性了,如果我们不加防范的话,其他用户的程序就能被恶意使用此功能的用户查看、删除,服务器的系统日志、系统文件也没有任何安全可言了。
解决方案
将FSO组件和删除或改名的方式我们不再过多的加以说明了,这一类的解决方法网络上已经有很多文章介绍了。
另外还有一种关于ASP的FSO组件漏洞的相应解决方案,即根据用户设置权限。在IIS里,可以设置每个站点的匿名访问所使用的帐号,默认为IUSR_ HostName,这一方法的原理就是针对每一个共享主机用户分别设置一个Windows帐号,如IUSR_HostName1,IUSR_ HostName 2等,然后将每一个用户限制在各自的Web目录下。
我们仔细的研究一下这种方案,可以发现这个方案无法真正实现安全。因为系统运行ASP时并不是使用的IUSR_ HostName帐号,而是IWAM_ HostName帐号,就象在ASP.NET中使用的用户ASPNET一样。也就是说每个ASP程序所拥有的权限并不是IUSR_ HostName的权限,而是IWAM_HostName用户的权限。这样的方法无法真正的将每个共享主机用户的文件系统访问权限限制在各自的虚拟站点中,每个用户仍然可以访问别人的代码。所以这种方法在ASP.NET中无法真正实现用户之间的安全性。
在ASP.NET中相应的运行ASP.NET程序的帐号为ASPNET,和上面所说的ASP中的解决方案类似,我们只能限制此用户不能访问系统目录等其他目录,但是无法防止用户访问其他共享主机用户的程序代码,无法从根本上杜绝这种问题。
那么,有没有真正的解决方案了呢?
有!这就是.NET Framework 的新特性――代码访问安全性
为了更好的理解这一问题的解决方法,我们需要先介绍一下.NET Framework的安全机制。然后再结合我们的实际问题来讨论解决方案。
为了解决安全问题,.NET Framework提供了一种称为代码访问安全性的安全机制。代码访问安全性允许根据代码的来源和代码的标识等属性将代码设置为不同级别的信任代码,同时还详细定义了不同级别的对代码的信任,从而可以详细的对代码设置各自的权限而不是将最大权限赋给所有的代码。使用代码访问安全性,可以减小恶意代码或各种错误的代码带来的严重的系统安全性问题的可能性。您可以设置允许代码执行的一组操作,同样可以设置永远不允许代码执行的一组操作。
实现代码访问安全性的基础就是JIT(运行时编译)和IL(中间代码)。所以所有以公共语言运行库为目标的托管代码都会受益于代码访问安全性。非托管代码则无法完全使用代码访问安全性。
下面我们将介绍一下代码访问安全性实现的各种功能:
代码访问安全性是控制代码对受保护资源和操作的访问权限的一种机制。在 .NET Framework中,代码访问安全性执行下列功能:
· 定义权限和权限集,它们表示访问各种系统资源的权限。
· 使管理员能够通过将权限集与代码组关联来配置安全策略。
· 使代码能够请求运行所需权限以及其他一些有用的权限,以及指定代码绝对不能拥有哪些权限。
· 根据代码请求的权限和安全策略允许的操作,向加载的每个程序集授予权限。
· 使代码能够要求其调用方拥有特定的权限。
· 使代码能够要求其调用方拥有数字签名,从而只允许特定组织或特定站点的调用方来调用受保护的代码。
· 通过将调用堆栈上每个调用方所授予的权限与调用方必须拥有的权限相比较,加强运行时对代码的限制。
为了确定是否已授予代码相应的权限,.NET运行库的安全系统将遍历整个调用堆栈,将每个调用方所授予的权限与目前要求的权限相比较。如果调用堆栈中的任何调用方没有要求的权限,则会引发安全性异常,并会拒绝访问和相应的操作。堆栈步旨在防止引诱攻击;在这种攻击中,受信程度较低的代码调用高度信任的代码,并使用高度信任的代码执行未经授权的操作。在运行时要求所有调用方都拥有权限将影响性能,但对防止代码遭受攻击至关重要。若要优化性能,可以使代码执行较少的堆栈步;但是,任何时候这样做时均必须确保不会暴露安全缺陷。
还存在另外一种代码访问安全性的常见用途,即应用程序将控件从网络 Web 站点直接下载到客户端,这种方式的代码安全性也是可以在客户端进行设置的,根据签名等数据权限证书来确定是不是可以允许下载的控件运行。这种方法类似于ActiveX的安全性设置,但是比之在设置权限更加详细和强大。同JAVA APPLET的沙箱安全机制相比,.NET 的客户端控件可以在本地简单设置后访问客户端的各种资源。由于这一方面的用途不是我们的重点,所以我们在这里就不再更详细的讨论其用途及其实现原理了。
下面我们就谈谈如何应用这一安全特性来解决ASP.NET中存在的系统安全漏洞。由于我们介绍的系统是共享主机,所以有其特殊性,即系统管理员无法事先给所有的代码赋予相应的权限,因为每个用户都可能有各种权限要求,并且这些要求特殊权力的代码在使用中都可能出现的,所以在权限管理上随时都有各种要求。
因此在权限设置方面,不仅仅是管理员设置,也包括了各个共享主机用户的权限请求,这也正是安全代码机制的一个重要部分。
请求权限是您让运行库知道代码执行有哪些操作权限的方法。通过将属性(声明式语法)放到代码的程序级范围来为程序集请求权限。
请求内置权限的代码示例:
//The attribute is placed on the assembly level.using System.Security.Permissions;[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]
将此段代码放在程序的开始部分(namespace声明之前),在编译时就会将请求的权限存储在程序集清单中。加载时,运行库检查权限请求,并应用安全策略规则来确定授予程序集哪些权限。
虽然我们编写的大部分代码都没有请求权限,其实不管是共享主机形式还是独立服务器形式都应该请求权限,这是因为请求权限有助于确保只将代码需要的权限授予代码。如果没有授予代码额外权限,即使某些恶意代码想利用您的代码来进行安全性破坏,它也无法操作没有赋给您自己代码相应权限的额外系统资源。您只应该请求代码需要的那些权限,而不应请求更多权限。
代码请求权限之后,系统管理员可以使用"权限查看"工具 (Permview.exe,位于您的.NET Framework的目录的bin目录下) 来检查您的程序集并根据其他条件来设置安全策略以决定是否给您的代码所请求的相应权限。如果您不显式地在代码中请求应用程序需要的权限,那么管理员将很难管理您的应用程序。在权限管理严格的主机上,将无法实现您的代码所要求的功能。
请求权限会通知运行库应用程序正常运行需要哪些权限,或具体不需要哪些权限。在.NET Framework安装后的默认状态下,所有代码都是FullTrust(完全信任)的。这时是不需要申请任何权限的,但是管理员一旦修改了代码安全,我们使用的磁盘访问就要受到限制了,这是就需要申请相应的权限了。我们上边介绍的文件管理代码就需要具有本地硬盘读写操作的能力,则应用程序必须拥有 FileIOPermission。如果代码不请求 FileIOPermission,在本地安全设置不允许应用程序拥有此权限的主机上,在应用程序尝试磁盘操作时就会引发安全性异常。即使应用程序能够处理此异常,也不会允许它操作磁盘。当然,如果您的代码不访问受保护的资源或执行受保护的操作,则不必请求任何权限。例如,如果代码只根据向它传递的输入来计算结果而不使用任何资源,则不必请求权限。如果您的代码访问受保护的资源但未请求必要的权限,则仍可能允许它执行,但如果它尝试访问某种资源而它又没有必要的权限,则可能在执行过程中失败。
系统管理员在得到了用户的权限申请后,可以根据情况考虑是否赋予用户相应的权限,在这里我们来看一下相应代码权限设置的具体方法。
在我们安装成功.NET Framework之后,在Windows 2000 Server的管理工具里多了两项管理工具: Microsoft .NET Framework Configration和Microsoft .NET Framework Wizards。这两种管理工具要实现的功能差不多,只不过Microsoft .NET Framework Wizards是通过向导方式设置,如果您对于.NET Framewrk的安全性操作不是很熟悉的话,可以使用向导根据系统提示一步步的来设置相关的权限。
Microsoft .NET Framework Wizards界面
Microsoft .NET Framework Wizards界面
使用Microsoft .NET Framework Wizards可以简单的设置.NET Framework的权限。但是我们为了更好的管理各个代码的权限,还是使用Microsoft .NET Framework Configuration来详细的设置我们所需的权限。
(Microsoft .NET Framework Configuration主界面)
在Microsoft .NET Framework Configuration中可以设置所有关于.NET Framework的属性。
点击我的电脑,打开下拉菜单,我们可以看到程序集缓存、已配置程序集、远程处理服务、运行库安全策略、应用程序等五项。运行库安全策略设置是我们这篇文章的重点。
我们可以先查看一下程序集缓存,在这里我们可以看到所有的全局程序集缓存,全局程序集缓存中存储了专门指定给由计算机中若干应用程序共享的程序集。在这里我们可以发现我们可以使用的所有的程序集,同时也可以添加和删除某些程序集。详细操作请参见.NET Framework SDK文档。
我们在这里主要讨论的是运行库安全策略。在此策略中,按层次结构由高到低分为四个级别,即:企业、计算机、用户、应用程序。在计算权限授予时,运行库从该层次结构的顶部开始,然后向下进行计算。较低的策略级别不能对在较高级别上授予的权限进行增加,但是可以使权限减少。这就是说如果我们将计算机策略设置为较小的权限时,可以不必更改企业策略就可以使设置的权限生效,也就是说权限检查的顺序是从低级别到高级别,只有在低级别中不存在的设置才会检查上一级的设置。默认情况下,用户策略和应用程序域策略的限制性小于计算机策略和企业级策略。大部分默认策略存在于计算机级别。所以我们需要将默认安装的主机的权限在计算机级别上进行修改,修改的内容根据主机是不是共享主机,主机应用的其他不明代码的可能性来设置。如果是我们讨论的共享主机的话,在计算机级别上就尽量将权限设的小一些,为了避免我们讨论的文件系统安全问题,一定要注意权限中的本地磁盘访问权限。
我们打开计算机策略设置可以发现几个默认的代码组、权限集和策略程序集。
根据需要,我们可以添加代码组和自定义的权限集。
在添加代码组的时候可以选择几种条件,主要的条件类型:默认为All Code、应用程序目录、哈希、强名称、作者、站点等。
对于我们所要讨论的共享主机,我们需要将My_Computer_Zone下的All Code的权限更改为不能进行磁盘读写,在更改之前,我们需要先定义一个权限集。这一权限集的作用就是将我们需要点击权限集,右键快捷菜单中选择新建,会出现一个创建权限集的窗口,这里需要给我们新建的权限集命名。下一步就是将单个权限分配给权限集。如下图所示。
在这里我们可以给这个新建的权限集赋予一个的系统权限,如上图所示,可用的权限包括:目录服务、DNS、事件日志、环境变量、文件IO、OLEDB数据库操作、注册表等等。我们主要要说明的是文件IO操作,其他的权限操作可以根据自己的需求来设置。这里我们就不再说明了。
在文件IO的权限设置中我们可以自定义针对每一个目录的权限,这里包括读、写、追加、路径盘等操作,在这里我们可以将我们需要的目录权限添加到列表中。因为我们是利用这一权限使所有没有配置权限的代码不可以进行文件IO操作,所以我们不强文件IO添加到分配的权限中。
新建了这一权限集后,我们更改一下默认设置,即将All Code的权限设置为此新建的权限集,也就是说所有没有在此定义代码都不能访问文件IO系统。
这里需要注意一件事情,因为Microsoft .NET Framework Configuration本身也需要文件IO权限,如果没有单独分配给Configuration一个文件IO操作权限的话,那么您就不能再次使用Configuration来设置权限了,只能重新安装.NET Framework了。所以我们需要将FullTrust权限分配给Configuration所使用的Dll,即mscorcfg.dll。在添加时,成员条件可以选择强名称,使用"导入",到winnt/window .net/framework/versionnumber/下选择mscorcfg.dll。如果需要运行其他配置程序,还需要设置相应的权限,这些系统程序一般都在系统程序集缓存中。
这样我们就完成了一个简单的设置,可以防止任何未经验证的代码访问文件IO系统。这样就从根本上防止了磁盘恶意操作。
如果您今后需要利用这一功能或者有共享主机用户需要使用文件IO功能,那么您可以在Microsoft .NET Framework Configuration中将其加入代码,如果不能使其使用其他功能,可以仅仅设置一个只具有文件IO权限的权限集。如果是共享主机用户您还可以给他分配直接到其所使用的目录的全部读写权限,对于他的日志文档,您可以将读功能分配给用户。通过上边新建权限集时我们可以发现:权限集可以规定到每一个目录的读写权限,所以可以将用户锁定于其可以使用的目录中。当然对于共享主机提供商来说,最好的方法就是自己实现这些功能,然后配置权限系统使用户使用共享主机提供商的程序来实现他们的正常操作,而避免了恶意文件操作。
需要注意的是如果分配给每一个单独的程序相应的权限时,我们最好使用强名称这一方式或者其他的可验证方式,强名称由程序集的标识--其简单文本名称、版本号和区域性信息(如果提供)--加上公钥和数字签名组成。这就需要我们使用Sn.exe 来设置密钥、签名和签名验证。强名称保证了程序是开发人员开发的并且没有被改动。
在进行上面的设置之后,管理员可以根据用户的各种需求来设置不同的代码集和权限集。
我们已经简单的介绍了一下ASP.NET中关于文件IO系统的漏洞的防治方法,这一方法有些繁琐,但是却可以从根本上杜绝一些漏洞,由于.NET的JIT(运行时编译)和IL(中间语言),.NET可以在程序编译时检查程序的安全性设置,所以能从根本上防止一些非法访问。.NET的代码安全性的内容很全面,我们讨论的只是很少的一部分,更多的功能需要大家共同来探索、学习。
开发环境:.Net 框架1.0 Version 1.0.3705
一、ASP.NET虚拟主机存在的重大隐患
我曾经在WWW.BRINKSTER.COM申请了一个免费的ASP.NET空间,上传了两个程序,其中一个查看目录和文件的程序证明我的判断:ASP共享空间服务器存在的一个安全问题,在 ASP+ 共享空间服务器中依然存在并且变得更加难以防范!通过这个程序我可以浏览所有用户的ASP+程序,可以查看服务器的系统日志……,当然,如果我想删除什么的话也不会有什么问题。为了让大家更清楚地了解这一问题,我们有必要简单介绍一下ASP中就已经存在的这一问题。
ASP中常用的标准组件:FileSystemObject,这个组件为 ASP 提供了强大的文件系统访问能力,可以对服务器硬盘上的任何有权限的目录和文件进行读写、删除、改名等操作。FSO对象来自微软提供的脚本运行库scrrun.dll中。
使用下面的代码就可以在ASP中创建一个FSO对象:
Set fso = CreateObject("Scripting.FileSystemObject")
我们使用fso对象包含的属性和方法,如Drive、Drives、Folder、Floders、File、Files等对服务器的磁盘、目录和文件进行读、写、删除等操作。这一强大的文件系统访问能力给ASP共享空间提供者带来了严重的安全问题,很多ASP空间的管理员都删除此组件或将这个组件改名以避免用户使用这一标准组件。删除组件或组件改名确实是一个简单的方法并且也很有效,但是却使广大用户无法使用它的强大的功能。网络上还有一种看起来很美的方案,它允许用户使用 FileSystemObject 组件又不影响服务器的安全,即对每一个用户都设置一个独立的服务器用户和单个目录的操作权限。但是这种方法是有问题的。因为ASP和ASP.NET中在这方面的问题十分类似,所以我们将在ASP.NET的相应解决办法部分详加说明。
在ASP.NET中我们发现这一问题仍然存在,并且变得更加难以解决。这是因为.NET中关于系统IO操作的功能变得更加强大,而使这一问题更严重的是ASP.NET所具有的一项新功能,这就组件不需要象ASP那样必须要使用regsvr32来注册了,只需将Dll类库文件上传到bin目录下就可以直接使用了。这一功能确实给开发ASP.NET带来了很大的方便,但是却使我们在ASP中将此dll删除或者改名的解决方法失去效用了,防范此问题就变得更加复杂。在讨论解决方案之前,我们先来看一下怎么来实现上述的危险的功能。
二、文件系统操作示例
在我们编写代码之前,有必要了解一下我们需要用到的几个主要的类。这几个类都在System.IO名称空间下,System.IO 名称空间包含允许在数据流和文件上进行同步和异步读写的类。
在整个应用程序的开始部分我们需要了解一下服务器的系统信息,这就需要用到System.Environment类,该类提供有关当前环境和平台的信息以及操作它们的方法。我们通过System.Environment类可以得到系统的当前目录和系统目录,这可以使我们更快的发现几个关键的目录;我们还可以通过获取运行当前进程的用户名来帮助我们了解ASP.NET程序运行所使用的用户,进一步设置用户权限以避免这一安全问题。
我们还要使用System.IO名称空间的其他几个类是:
System.IO.Directory:提供用于创建、移动和枚举通过目录和子目录的静态方法的类
System.IO.File:提供用于创建、复制、删除、移动和打开文件的静态方法的类
System.IO.FileInfo:提供创建、复制、删除、移动和打开文件的实例方法的类
System.IO.StreamReader:实现一个 TextReader,使其以一种特定的编码从字节流中读取字符。
每个我们所使用的类的属性和方法的具体用法我们将以代码注释的方式在程序中加以说明。
System.IO名称空间在 .NET FRAMEWORK提供的mscorlib.dll中,在使用VS.Net编程之前需要将此Dll引用到此项目中。
我们所编写的程序都使用了Codebehind方式,即每一个aspx程序都有一个对应的aspx.cs程序,aspx程序中只是写与页面显示相关的代码,所有逻辑实现的代码都放在相应的aspx.cs文件中,这样就可以更好得做到显示与逻辑的分离。由于我们的目的不是讨论Codebehind技术,所以就不在对此多加讨论了。
在这篇文章里,我们只介绍几个主要的类及其关键方法的用法,详细程序请查看附带的源代码。
程序一:显示服务器的当前信息和全部逻辑驱动器的名称的程序listdrivers.aspx
主要方法1:我们使用 GetSysInf() 方法来得到服务器的当前环境和平台的信息
//获取系统信息的方法,此方法在listdrivers.aspx.cs文件中public void GetSysInf () {//获取操作系统类型qDrives = Environment.OSVersion.ToString();//获取系统文件夹qSystemDir = Environment.SystemDirectory.ToString();/*获取映射到进程上下文的物理内存量,通过这一内存映射量可以了解ASP.NET程序在运行时需要多少系统物理内存,有助于更好的规划我们的整个应用,因为物理内存量是以Byte为单位的,所以我们将此数值除以1024,可以得到单位为KB的物理内存量*/qMo = (Environment.WorkingSet/1024).ToString();//获取当前目录(即该进程从中启动的目录)的完全限定路径qCurDir = Environment.CurrentDirectory.ToString();//获取主机的网络域名qDomName = Environment.UserDomainName.ToString();//获取系统启动后经过的毫秒数qTick = Environment.TickCount;
//计算得到系统启动后经过的分钟数qTick /= 60000;//获取机器名qMachine = Environment.MachineName;//获取运行当前进程的用户名qUser = Environment.UserName;/*检索此计算机上格式为"<驱动器号>:"的逻辑驱动器的名称,返回字符串数组,这是下一步操作的关键所在*/achDrives = Directory.GetLogicalDrives();//获取此字符串数组的维数,确定有多少个逻辑驱动器nNumOfDrives = achDrives.Length;}
系统信息不需要进行操作,我们简单的用asp:Label将他们显示出来就行了。逻辑驱动器的个数在不同的服务器上是不定的,所以用不定长数组保存逻辑驱动器的名称,而且逻辑驱动器的名称也是我们下一步浏览目录和文件的基础,故我们采用了数据网格DataGrid来显示和处理它。
显示和处理逻辑驱动器名称的DataGrid的代码(代码在listdrivers.aspx文件):
<asp:DataGrid id="DriversGrid" runat="server" AutoGenerateColumns="false"><Columns><asp:BoundColumn HeaderText="ID" DataField="ID" /><asp:BoundColumn HeaderText="磁盘名" DataField="Drivers" /><asp:HyperLinkColumnHeaderText="详细信息" DataNavigateUrlField="Drivers" DataNavigateUrlFormatString="listdir.aspx?dir={0}" DataTextField="Detail" Target="_new" /></Columns></asp:DataGrid>
前两个BoundColumn列都是显示序号和实际逻辑驱动器名称的,需要说明的是第三列,我们在进入各个逻辑驱动器显示目录和文件之前需要将所选择的逻辑驱动器的名称传递到显示目录的文件去,所以需要一个特殊的超级链接行HyperLinkColumn,我们将DataNavigateUrlField设置为数据源中要绑定到 HyperLinkColumn 中的超级链接的 URL 的字段,在此即逻辑驱动器名称。然后将DataNavigateUrlFormatString设置为当 URL 数据绑定到数据源中的字段时,此HyperLinkColumn中的超级链接的 URL 的显示格式,即要链接到的下一级处理页面,在此为listdir.aspx?dir={用户点击行的逻辑驱动器名称}
创建数据源的代码(代码在listdrivers.aspx.cs文件中):
//通过此方法返回一个集合形式的数据视图DataViewICollection CreateDataSource() {//定义内存中的数据表DataTableDataTable dt = new DataTable();//定义DataTable中的一行数据DataRowDataRow dr;/*向DataTable中增加一个列,格式:DataColumn("Column", type)Column为数据列的名字,type为数据列的数据类型*/dt.Columns.Add(new DataColumn("ID", typeof(Int32)));dt.Columns.Add(new DataColumn("drivers", typeof(string)));dt.Columns.Add(new DataColumn("detail", typeof(string)));//使用for循环将逻辑驱动器的名称以行的形式添加到数据表DataTable中for (int i = 0; i < nNumOfDrives; i++) {//定义新行dr = dt.NewRow();//对行中每列进行赋值,注意要与上边定义的DataTable的行相对应dr[0] = i; //循环生成的序号dr[1] = achDrives.ToString(); //逻辑驱动器的名称dr[2] = "查看详情";//向DataTable中添加行dt.Rows.Add(dr);}//根据得到的DataTable生成自定义视图DataViewDataView dv = new DataView(dt);//返回得到的视图DataViewreturn dv;}
我们通过这个方法得到了一个包含所有我们需要的数据的数据视图DataView,我们只需要在此aspx页的Page_Load方法中将此数据视图绑定到DataGrid上就可以了。
数据绑定代码(代码在listdrivers.aspx.cs文件中):
/* 设置DataGrid的数据源DataSource为我们从CreateDataSource()方法得到的数据视图DataView */DriversGrid.DataSource = CreateDataSource();//将此DataGrid进行数据绑定DriversGrid.DataBind();
通过上边介绍的几种主要方法我们就实现了获取系统信息和显示所有逻辑驱动器名称的功能,并且可以通过相应的链接进入下一个显示目录和文件名的程序listdir.aspx显示该逻辑驱动器下的所有目录和文件。
程序二:显示目录中所有子目录和文件的程序listdir.aspx
目录下有子目录和文件两种形式,必须分别对待。我们调用此程序本身对子目录进行列表显示,而文件我们需要调用showfile.aspx程序对文件的属性和内容进行显示。并且两者还有不同的删除方法,所以我们在这里设置了两个DataGrid,两个DataTable,两个DataView,分别处理和显示目录和文件。
显示和处理目录和文件的DataGrid的代码(代码在listdir.aspx文件):
显示目录或文件的序号和名称的数据列类似于listdrivers.aspx程序中的相应代码,这里就不再重复了。对于子目录和文件分别有各自的处理页面,所以需要导航到两个不同的页面,对于子目录,我们继续使用listdir.aspx程序对其下的子目录和文件进行列表显示:
<asp:HyperLinkColumn DataNavigateUrlField="DirName"DataNavigateUrlFormatString="listdir.aspx?dir={0}"DataTextField="DirDetail" HeaderText="详细信息"Target="_new"/>
对于文件,我们使用showfile.aspx程序显示其属性和内容:
<asp:HyperLinkColumn DataNavigateUrlField="FileName"DataNavigateUrlFormatString="showfile.aspx?file={0}" DataTextField="FileDetail" HeaderText="详细信息" Target="_new" />
在两个DataGrid(DirGrid,FileGrid)中我们分别设置了两个HyperLinkColumn列来导航到不同的处理页面。
在两个DataGrid中我们都使用了一个删除的按钮列:
<asp:ButtonColumn HeaderText="删除" Text="删除" CommandName="Delete"/>
由于添加、更新、删除功能列都是DataGrid的默认模板列,所以可以在Vs.net中通过DataGrid的属性生成器自动添加此列。
获取上一页面所传递来的参数的代码:
因为在下面产生数据源的方法中需要使用由上一个页面传递过来的参数来确定目录和文件的名称,所以在页面的Page_Load方法里使用了下列代码:
strDir2List = Request.QueryString["dir"];
字符串strDir2List即传过来的目录名或文件名。
因为我们使用了两个DateGrid,就需要进行两次数据绑定,就有两个不同的生成数据源的方法。
生成目录数据网格(DirGrid)数据源的方法:
//通过此方法返回一个集合形式的数据视图DataView,用来初始化子目录的DataGridICollection CreateDataSourceDir() {dtDir = new DataTable();DataRow dr;//向DataTable中添加新的数据列,共四列dtDir.Columns.Add(new DataColumn("DirID", typeof(Int32)));dtDir.Columns.Add(new DataColumn("DirName", typeof(string)));dtDir.Columns.Add(new DataColumn("DelDir", typeof(string)));dtDir.Columns.Add(new DataColumn("DirDetail", typeof(string))); //根据传入的参数(目录名)得到此目录下所有子目录名的字符串数组string [] DirEntries = Directory.GetDirectories(strDir2List);//使用foreach循环可以对未知长度的数组进行遍历循环foreach(string DirName in DirEntries){dr = dtDir.NewRow();dr[0] = i;//序号dr[1] = DirName;//文件夹名称 dr[3] = "删除";dr[3] = "查看详情";dtDir.Rows.Add(dr);i++; }DataView dvDir = new DataView(dtDir);//返回得到的数据视图return dvDir;}生成文件数据网格(FileGrid)数据源的方法://通过此方法返回一个集合形式的数据视图DataView,用来初始化文件的DataGridICollection CreateDataSourceFile() {dtFile = new DataTable();DataRow dr;dtFile.Columns.Add(new DataColumn("FileID", typeof(Int32)));dtFile.Columns.Add(new DataColumn("FileName", typeof(string)));dtFile.Columns.Add(new DataColumn("DelFile", typeof(string)));dtFile.Columns.Add(new DataColumn("FileDetail", typeof(string))); //根据传入的参数(目录名)得到此目录下所有文件名的字符串数组string [] FileEntries = Directory.GetFiles(strDir2List);foreach(string FileName in FileEntries){dr = dtFile.NewRow();dr[0] = i;dr[1] = FileName; dr[2] = "删除";dr[3] = "查看详情";dtFile.Rows.Add(dr);i++;}dvFile = new DataView(dtFile);return dvFile;}
我们编程实现了两个DataSource只需在页面的Page_Load方法里对两个DataGrid进行数据绑定即可将得到的DataTable中的数据显示在aspx页面的DataGrid上。
数据绑定代码:
//对子目录数据列表DirGrid进行数据源定义和数据绑定DirGrid.DataSource = CreateDataSourceDir();DirGrid.DataBind(); //对文件数据列表FileGrid进行数据源定义和数据绑定FileGrid.DataSource = CreateDataSourceFile();FileGrid.DataBind();
通过我们上边介绍的主要方法,我们实现了对某个逻辑驱动器或目录中的所有子目录和文件进行了列表显示,并且可以根据显示结果更进一步的浏览子目录或者查看文件的属性和内容提要。浏览子目录仍然是通过listdir.aspx这个程序,没有任何子目录级别要求,没有目录深度限制。
删除子目录和文件的主要方法和代码:
在删除子目录时,我们需要用到Directory.Delete (string,bool)方法,此方法有两种:
1.public static void Delete(string);
从指定路径删除空目录。
2.public static void Delete(string, boolean);
删除指定的目录并(如果指示)删除该目录中的任何子目录,将boolean设置为true的话,则删除此目录下的所有子目录和文件,否则将boolean设置为false。
在这里我们使用了第二种方法,如果选择删除的话,将删除此目录下的所有子目录和文件。
注意:Directory 类的所有方法都是静态的,因而无需具有目录Directory的实例就可被调用。
/*实现删除子目录的方法,此方法为VS.NET自动添加,注意DataGridCommandEventArgs e为DirGrid中 CommandName="Delete" 的ButtonColumn的事件,通过此事件,我们可以得到是那一行的ButtonColumn按钮列被点击,进而确定我们需要删除的子目录的名称*/private void DirGrid_DeleteCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e){/*定义一个单元格,e.Item为此事件所发生行的所有项目,e.Item.Cells[1]为整个行的第二个单元格的内容,在此DataGrid中为子目录的名称*/TableCell ItemCell = e.Item.Cells[1];//得到此子目录的名称的字符串string item = ItemCell.Text;//删除此子目录Directory.Delete(item,true);//删除后进行数据绑定以更新数据列表DirGrid.DataBind();}
在删除文件时,我们需要用到File.Delete(string path);
注意:File 类的所有方法都是静态的,因而无需具有目录的实例就可被调用。
private void FileGrid_DeleteCommand(object source,System.Web.UI.WebControls.DataGridCommandEventArgs e) {TableCell ItemCell = e.Item.Cells[1];//得到此文件名称的字符串string item = ItemCell.Text;//删除此文件File.Delete(item);//删除后进行数据绑定以更新数据列表DirGrid.DataBind();}
通过上边的主要方法我们在页面上实现了一个删除某一个子目录或者文件的功能,此功能在测试时需要慎重使用,一旦删除无法通过常规方法恢复。其他如目录或文件改名、修改内容等方法都可以在此程序基础上添加相应的功能,实现方法也很简单。各位爱好者可以通过添加相应功能,使之扩充为一个基于Web的服务器文件管理系统。我们也可以由此看到这个程序的危害性,一个没有对此安全隐患采取防范措施的服务器的文件系统就都暴露在了使用此程序的用户面前。
程序三:显示文件属性和内容的程序showfile.aspx
在显示属性和内容时需要用到的两个主要的类:
System.IO.FileInfo:提供创建、复制、删除、移动和打开文件的实例方法,并且帮助创建 FileStream 对象。
System.IO.StreamReader:实现一个 TextReader,使其以一种特定的编码从字节流中读取字符。除非另外指定,StreamReader的默认编码为 UTF-8,而不是当前系统的 ANSI 代码页。UTF-8 可以正确处理 Unicode 字符并在操作系统的本地化版本上提供一致的结果。
Showfile.aspx页面主要代码:
<asp:Label id="FileDetail" runat="server"/>
我们只是将文件的属性信息和部分内容显示在此Label上。所以没有其他复杂的代码。
获取文件信息和内容的主要代码都在Page_Load方法中(代码在showfile.aspx.cs文件中):
//接收传入的参数,确定需要操作的文件名称strFile2Show = Request.QueryString["file"];//根据文件名实例化一个FileInfo对象FileInfo fi = new FileInfo(strFile2Show);FileDetail.Text = "文件名:";FileDetail.Text += strFile2Show+"<br>";FileDetail.Text += "文件大小";//获得文件的大小,然后变换单位为KBFileDetail.Text += (fi.Length/1024).ToString()+"K<br>";FileDetail.Text += "创建文件时间:";//获得文件的创建日期FileDetail.Text += fi.CreationTime.ToString();FileDetail.Text += "上次访问时间:";//获得文件的上次访问日期FileDetail.Text += fi.LastAccessTime.ToString()+"<br>";FileDetail.Text += "上次写入时间:";//获得文件的上次写入日期FileDetail.Text += fi.LastWriteTime.ToString()+"<br>";//实例化一个StreamReader对象,用于读取此FileInfo的内容StreamReader FileReader = fi.OpenText();//定义一个长度为1000的字符数组作为缓冲区char[] theBuffer = new char[1000];/*ReadBlock方法:从当前流中读取最大数量的字符并从索引开始将该数据写入缓冲区。参数:char[] buffer:方法返回时,包含指定的字符数组int index:buffer 中开始写入的位置int count:最多读取的字符数*/int nRead = FileReader.ReadBlock(theBuffer,0,1000);FileDetail.Text += new String(theBuffer,0,nRead);//关闭此 StreamReader 并释放与之关联的所有系统资源FileReader.Close();
到目前为止,我们实现了一个简单的web页面的服务器磁盘管理应用程序,可以查看、删除目录和文件。如果需要修改文件、新建文件和文件夹等功能,只需稍作修改,添加上相应的代码就可以。由于我们只是通过这个程序说明服务器中存在的安全隐患,所以在这里就不再实现这些功能了。
通过这三个简单的程序,我想大家已经能够清楚的认识到这一漏洞的危害性了,如果我们不加防范的话,其他用户的程序就能被恶意使用此功能的用户查看、删除,服务器的系统日志、系统文件也没有任何安全可言了。
解决方案
将FSO组件和删除或改名的方式我们不再过多的加以说明了,这一类的解决方法网络上已经有很多文章介绍了。
另外还有一种关于ASP的FSO组件漏洞的相应解决方案,即根据用户设置权限。在IIS里,可以设置每个站点的匿名访问所使用的帐号,默认为IUSR_ HostName,这一方法的原理就是针对每一个共享主机用户分别设置一个Windows帐号,如IUSR_HostName1,IUSR_ HostName 2等,然后将每一个用户限制在各自的Web目录下。
我们仔细的研究一下这种方案,可以发现这个方案无法真正实现安全。因为系统运行ASP时并不是使用的IUSR_ HostName帐号,而是IWAM_ HostName帐号,就象在ASP.NET中使用的用户ASPNET一样。也就是说每个ASP程序所拥有的权限并不是IUSR_ HostName的权限,而是IWAM_HostName用户的权限。这样的方法无法真正的将每个共享主机用户的文件系统访问权限限制在各自的虚拟站点中,每个用户仍然可以访问别人的代码。所以这种方法在ASP.NET中无法真正实现用户之间的安全性。
在ASP.NET中相应的运行ASP.NET程序的帐号为ASPNET,和上面所说的ASP中的解决方案类似,我们只能限制此用户不能访问系统目录等其他目录,但是无法防止用户访问其他共享主机用户的程序代码,无法从根本上杜绝这种问题。
那么,有没有真正的解决方案了呢?
有!这就是.NET Framework 的新特性――代码访问安全性
为了更好的理解这一问题的解决方法,我们需要先介绍一下.NET Framework的安全机制。然后再结合我们的实际问题来讨论解决方案。
为了解决安全问题,.NET Framework提供了一种称为代码访问安全性的安全机制。代码访问安全性允许根据代码的来源和代码的标识等属性将代码设置为不同级别的信任代码,同时还详细定义了不同级别的对代码的信任,从而可以详细的对代码设置各自的权限而不是将最大权限赋给所有的代码。使用代码访问安全性,可以减小恶意代码或各种错误的代码带来的严重的系统安全性问题的可能性。您可以设置允许代码执行的一组操作,同样可以设置永远不允许代码执行的一组操作。
实现代码访问安全性的基础就是JIT(运行时编译)和IL(中间代码)。所以所有以公共语言运行库为目标的托管代码都会受益于代码访问安全性。非托管代码则无法完全使用代码访问安全性。
下面我们将介绍一下代码访问安全性实现的各种功能:
代码访问安全性是控制代码对受保护资源和操作的访问权限的一种机制。在 .NET Framework中,代码访问安全性执行下列功能:
· 定义权限和权限集,它们表示访问各种系统资源的权限。
· 使管理员能够通过将权限集与代码组关联来配置安全策略。
· 使代码能够请求运行所需权限以及其他一些有用的权限,以及指定代码绝对不能拥有哪些权限。
· 根据代码请求的权限和安全策略允许的操作,向加载的每个程序集授予权限。
· 使代码能够要求其调用方拥有特定的权限。
· 使代码能够要求其调用方拥有数字签名,从而只允许特定组织或特定站点的调用方来调用受保护的代码。
· 通过将调用堆栈上每个调用方所授予的权限与调用方必须拥有的权限相比较,加强运行时对代码的限制。
为了确定是否已授予代码相应的权限,.NET运行库的安全系统将遍历整个调用堆栈,将每个调用方所授予的权限与目前要求的权限相比较。如果调用堆栈中的任何调用方没有要求的权限,则会引发安全性异常,并会拒绝访问和相应的操作。堆栈步旨在防止引诱攻击;在这种攻击中,受信程度较低的代码调用高度信任的代码,并使用高度信任的代码执行未经授权的操作。在运行时要求所有调用方都拥有权限将影响性能,但对防止代码遭受攻击至关重要。若要优化性能,可以使代码执行较少的堆栈步;但是,任何时候这样做时均必须确保不会暴露安全缺陷。
还存在另外一种代码访问安全性的常见用途,即应用程序将控件从网络 Web 站点直接下载到客户端,这种方式的代码安全性也是可以在客户端进行设置的,根据签名等数据权限证书来确定是不是可以允许下载的控件运行。这种方法类似于ActiveX的安全性设置,但是比之在设置权限更加详细和强大。同JAVA APPLET的沙箱安全机制相比,.NET 的客户端控件可以在本地简单设置后访问客户端的各种资源。由于这一方面的用途不是我们的重点,所以我们在这里就不再更详细的讨论其用途及其实现原理了。
下面我们就谈谈如何应用这一安全特性来解决ASP.NET中存在的系统安全漏洞。由于我们介绍的系统是共享主机,所以有其特殊性,即系统管理员无法事先给所有的代码赋予相应的权限,因为每个用户都可能有各种权限要求,并且这些要求特殊权力的代码在使用中都可能出现的,所以在权限管理上随时都有各种要求。
因此在权限设置方面,不仅仅是管理员设置,也包括了各个共享主机用户的权限请求,这也正是安全代码机制的一个重要部分。
请求权限是您让运行库知道代码执行有哪些操作权限的方法。通过将属性(声明式语法)放到代码的程序级范围来为程序集请求权限。
请求内置权限的代码示例:
//The attribute is placed on the assembly level.using System.Security.Permissions;[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]
将此段代码放在程序的开始部分(namespace声明之前),在编译时就会将请求的权限存储在程序集清单中。加载时,运行库检查权限请求,并应用安全策略规则来确定授予程序集哪些权限。
虽然我们编写的大部分代码都没有请求权限,其实不管是共享主机形式还是独立服务器形式都应该请求权限,这是因为请求权限有助于确保只将代码需要的权限授予代码。如果没有授予代码额外权限,即使某些恶意代码想利用您的代码来进行安全性破坏,它也无法操作没有赋给您自己代码相应权限的额外系统资源。您只应该请求代码需要的那些权限,而不应请求更多权限。
代码请求权限之后,系统管理员可以使用"权限查看"工具 (Permview.exe,位于您的.NET Framework的目录的bin目录下) 来检查您的程序集并根据其他条件来设置安全策略以决定是否给您的代码所请求的相应权限。如果您不显式地在代码中请求应用程序需要的权限,那么管理员将很难管理您的应用程序。在权限管理严格的主机上,将无法实现您的代码所要求的功能。
请求权限会通知运行库应用程序正常运行需要哪些权限,或具体不需要哪些权限。在.NET Framework安装后的默认状态下,所有代码都是FullTrust(完全信任)的。这时是不需要申请任何权限的,但是管理员一旦修改了代码安全,我们使用的磁盘访问就要受到限制了,这是就需要申请相应的权限了。我们上边介绍的文件管理代码就需要具有本地硬盘读写操作的能力,则应用程序必须拥有 FileIOPermission。如果代码不请求 FileIOPermission,在本地安全设置不允许应用程序拥有此权限的主机上,在应用程序尝试磁盘操作时就会引发安全性异常。即使应用程序能够处理此异常,也不会允许它操作磁盘。当然,如果您的代码不访问受保护的资源或执行受保护的操作,则不必请求任何权限。例如,如果代码只根据向它传递的输入来计算结果而不使用任何资源,则不必请求权限。如果您的代码访问受保护的资源但未请求必要的权限,则仍可能允许它执行,但如果它尝试访问某种资源而它又没有必要的权限,则可能在执行过程中失败。
系统管理员在得到了用户的权限申请后,可以根据情况考虑是否赋予用户相应的权限,在这里我们来看一下相应代码权限设置的具体方法。
在我们安装成功.NET Framework之后,在Windows 2000 Server的管理工具里多了两项管理工具: Microsoft .NET Framework Configration和Microsoft .NET Framework Wizards。这两种管理工具要实现的功能差不多,只不过Microsoft .NET Framework Wizards是通过向导方式设置,如果您对于.NET Framewrk的安全性操作不是很熟悉的话,可以使用向导根据系统提示一步步的来设置相关的权限。
Microsoft .NET Framework Wizards界面
Microsoft .NET Framework Wizards界面
使用Microsoft .NET Framework Wizards可以简单的设置.NET Framework的权限。但是我们为了更好的管理各个代码的权限,还是使用Microsoft .NET Framework Configuration来详细的设置我们所需的权限。
(Microsoft .NET Framework Configuration主界面)
在Microsoft .NET Framework Configuration中可以设置所有关于.NET Framework的属性。
点击我的电脑,打开下拉菜单,我们可以看到程序集缓存、已配置程序集、远程处理服务、运行库安全策略、应用程序等五项。运行库安全策略设置是我们这篇文章的重点。
我们可以先查看一下程序集缓存,在这里我们可以看到所有的全局程序集缓存,全局程序集缓存中存储了专门指定给由计算机中若干应用程序共享的程序集。在这里我们可以发现我们可以使用的所有的程序集,同时也可以添加和删除某些程序集。详细操作请参见.NET Framework SDK文档。
我们在这里主要讨论的是运行库安全策略。在此策略中,按层次结构由高到低分为四个级别,即:企业、计算机、用户、应用程序。在计算权限授予时,运行库从该层次结构的顶部开始,然后向下进行计算。较低的策略级别不能对在较高级别上授予的权限进行增加,但是可以使权限减少。这就是说如果我们将计算机策略设置为较小的权限时,可以不必更改企业策略就可以使设置的权限生效,也就是说权限检查的顺序是从低级别到高级别,只有在低级别中不存在的设置才会检查上一级的设置。默认情况下,用户策略和应用程序域策略的限制性小于计算机策略和企业级策略。大部分默认策略存在于计算机级别。所以我们需要将默认安装的主机的权限在计算机级别上进行修改,修改的内容根据主机是不是共享主机,主机应用的其他不明代码的可能性来设置。如果是我们讨论的共享主机的话,在计算机级别上就尽量将权限设的小一些,为了避免我们讨论的文件系统安全问题,一定要注意权限中的本地磁盘访问权限。
我们打开计算机策略设置可以发现几个默认的代码组、权限集和策略程序集。
根据需要,我们可以添加代码组和自定义的权限集。
在添加代码组的时候可以选择几种条件,主要的条件类型:默认为All Code、应用程序目录、哈希、强名称、作者、站点等。
对于我们所要讨论的共享主机,我们需要将My_Computer_Zone下的All Code的权限更改为不能进行磁盘读写,在更改之前,我们需要先定义一个权限集。这一权限集的作用就是将我们需要点击权限集,右键快捷菜单中选择新建,会出现一个创建权限集的窗口,这里需要给我们新建的权限集命名。下一步就是将单个权限分配给权限集。如下图所示。
在这里我们可以给这个新建的权限集赋予一个的系统权限,如上图所示,可用的权限包括:目录服务、DNS、事件日志、环境变量、文件IO、OLEDB数据库操作、注册表等等。我们主要要说明的是文件IO操作,其他的权限操作可以根据自己的需求来设置。这里我们就不再说明了。
在文件IO的权限设置中我们可以自定义针对每一个目录的权限,这里包括读、写、追加、路径盘等操作,在这里我们可以将我们需要的目录权限添加到列表中。因为我们是利用这一权限使所有没有配置权限的代码不可以进行文件IO操作,所以我们不强文件IO添加到分配的权限中。
新建了这一权限集后,我们更改一下默认设置,即将All Code的权限设置为此新建的权限集,也就是说所有没有在此定义代码都不能访问文件IO系统。
这里需要注意一件事情,因为Microsoft .NET Framework Configuration本身也需要文件IO权限,如果没有单独分配给Configuration一个文件IO操作权限的话,那么您就不能再次使用Configuration来设置权限了,只能重新安装.NET Framework了。所以我们需要将FullTrust权限分配给Configuration所使用的Dll,即mscorcfg.dll。在添加时,成员条件可以选择强名称,使用"导入",到winnt/window .net/framework/versionnumber/下选择mscorcfg.dll。如果需要运行其他配置程序,还需要设置相应的权限,这些系统程序一般都在系统程序集缓存中。
这样我们就完成了一个简单的设置,可以防止任何未经验证的代码访问文件IO系统。这样就从根本上防止了磁盘恶意操作。
如果您今后需要利用这一功能或者有共享主机用户需要使用文件IO功能,那么您可以在Microsoft .NET Framework Configuration中将其加入代码,如果不能使其使用其他功能,可以仅仅设置一个只具有文件IO权限的权限集。如果是共享主机用户您还可以给他分配直接到其所使用的目录的全部读写权限,对于他的日志文档,您可以将读功能分配给用户。通过上边新建权限集时我们可以发现:权限集可以规定到每一个目录的读写权限,所以可以将用户锁定于其可以使用的目录中。当然对于共享主机提供商来说,最好的方法就是自己实现这些功能,然后配置权限系统使用户使用共享主机提供商的程序来实现他们的正常操作,而避免了恶意文件操作。
需要注意的是如果分配给每一个单独的程序相应的权限时,我们最好使用强名称这一方式或者其他的可验证方式,强名称由程序集的标识--其简单文本名称、版本号和区域性信息(如果提供)--加上公钥和数字签名组成。这就需要我们使用Sn.exe 来设置密钥、签名和签名验证。强名称保证了程序是开发人员开发的并且没有被改动。
在进行上面的设置之后,管理员可以根据用户的各种需求来设置不同的代码集和权限集。
我们已经简单的介绍了一下ASP.NET中关于文件IO系统的漏洞的防治方法,这一方法有些繁琐,但是却可以从根本上杜绝一些漏洞,由于.NET的JIT(运行时编译)和IL(中间语言),.NET可以在程序编译时检查程序的安全性设置,所以能从根本上防止一些非法访问。.NET的代码安全性的内容很全面,我们讨论的只是很少的一部分,更多的功能需要大家共同来探索、学习。