在关于web应用程序安全的思考(序)中我曾提到﹕web应用程序的安全不应该依赖于客户端的请求信息。 众所周知﹐http协议是开放的﹐因此谁都能向网络上公开的web服务器发送request请求﹐要求一个URL(Uniform Resource Locator 统一资源定位符)。 所谓request﹐不过是符合http协议(即遵守http请求语法)的一大段字符串而已﹕ 下面是一个aspx的请求示例﹕ GET /FrameWorkService/TestRequest.aspx HTTP/1.1
Connection: Keep-Alive Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-tw Host: localhost User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30) UA-CPU: x86
下面是一个web service的请求示例﹕ POST /testwssecurity/service2.asmx HTTP/1.1
Content-Length: 288 Content-Type: text/xml; charset=utf-8 Expect: 100-continue Host: localhost User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol 2.0.50727.42) SOAPAction: "http:///HelloWorld" <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas./soap/envelope/" xmlns:xsi="http://www./2001/XMLSchema-instance" xmlns:xsd="http://www./2001/XMLSchema"><soap:Body><HelloWorld xmlns="http:///" /></soap:Body></soap:Envelope>
相信大家基本上能理解上述字符串的意义。这表明我们只要组织类似的字符串﹐然后发往相应的web服务器﹐就可以请求到某个URL了﹐也就是说web请求不依赖浏览器(其实web也不依赖服务器﹐它只依赖http协议)。 下面的这个程序是C#写的通过socket直接向web服务器发送http请求的示例﹕
1using System;
2using System.Text; 3using System.IO; 4using System.Net; 5using System.Net.Sockets; 6 7public class server 8{ 9 //建立socket連接 10 private static Socket ConnectSocket(string server, int port) 11 { 12 Socket s = null; 13 IPHostEntry hostEntry = null; 14 hostEntry = Dns.GetHostEntry(server); 15 foreach (IPAddress address in hostEntry.AddressList) 16 { 17 IPEndPoint ipe = new IPEndPoint(address, port); 18 Socket tempSocket = 19 new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 20 tempSocket.Connect(ipe); 21 if (tempSocket.Connected) 22 { 23 s = tempSocket; 24 break; 25 } 26 else 27 { 28 continue; 29 } 30 } 31 Console.WriteLine(s==null?"":"連接建立成功﹗"); 32 return s; 33 } 34 35 //發送request請求并返回響應字串 36 private static string SocketSendReceive(string request,string server, int port) 37 { 38 Byte[] bytesSent = Encoding.ASCII.GetBytes(request); 39 Byte[] bytesReceived = new Byte[256]; 40 Socket s = ConnectSocket(server, port); 41 if (s == null) 42 return ("連接失敗﹗"); 43 Console.WriteLine("正在發送請求"); 44 s.Send(bytesSent, bytesSent.Length, 0); 45 int bytes = 0; 46 StringBuilder responsestr = new StringBuilder(); 47 Console.WriteLine("正在接收web服務器的回應"); 48 do 49 { 50 bytes = s.Receive(bytesReceived, bytesReceived.Length, 0); 51 responsestr.Append(Encoding.UTF8.GetString(bytesReceived, 0, bytes)); 52 } 53 while (bytes > 0); 54 return responsestr.ToString(); 55 } 56 57 //獲取Request請求字符串 58 private static string getRequestStr() 59 { 60 StringBuilder sb = new StringBuilder(); 61 sb.Append("GET /FrameWorkService/TestRequest.aspx?name=zkw&age=24 HTTP/1.1\r\n"); 62 sb.Append("Host: localhost\r\n"); 63 sb.Append("Accept: */*\r\n"); 64 sb.Append("Accept-Encoding: gzip, deflate\r\n"); 65 sb.Append("Accept-Language: zh-tw\r\n"); 66 sb.Append("User-Agent: Mozilla/8.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)\r\n"); 67 sb.Append("UA-CPU: x86\r\n"); 68 sb.Append("Cookie: ASP.NET_SessionId=g5vz3k55q4dhgy3dvmm3dj4x\r\n"); 69 sb.Append("Connection: Close\r\n\r\n"); 70 return sb.ToString(); 71 } 72 73 public static void Main(string[] args) 74 { 75 string requeststr = getRequestStr(); 76 Console.WriteLine("請求字串如下﹕\n{0}",requeststr); 77 string result = SocketSendReceive(requeststr,"localhost",80); 78 Console.WriteLine(result); 79 Console.ReadLine(); 80 } 81}
相關的aspx.cs程序如下﹕ using System;
using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using System.IO; public partial class TestRequest : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Request.SaveAs("c:/test.txt",true); using(StreamReader sr = new StreamReader("c:/test.txt")) { tt_request.Value = (sr.ReadToEnd()); } foreach (string key in Request.QueryString.AllKeys) div_querystring.Value += string.Format("{0}:{1}\r\n", key, Request[key]); if (Session["firsttime"] == null) { Session["firsttime"] = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); Response.Write("<b style=‘color:red‘>first request</b></br>"); } Response.Write("First Time:" + Session["firsttime"].ToString()); } } aspx頁面: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="TestRequest.aspx.cs" Inherits="TestRequest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www./1999/xhtml" > <head runat="server"> <title>請求字串提取示例</title> </head> <body> 這是Request字符串﹕<br /> <textarea style="width:100%;height:200px" id="tt_request" runat="server"> </textarea> 以下是程式直接提取的參數﹕<br /> <textarea id="div_querystring" runat="server" style="width:100%;height:100px"> </textarea> </body> </html>
由上可知﹐web服务器对于请求方的识别能力是很低的。因此作为web应用程序安全管控的唯一依据就只能是request的url了﹐因为只有它才是真实的﹐而我们进行安全管控的最终目的也就是 这就是我抽象出来的web安全管控的本质﹐依据这点﹐我们就可以把web安全管控和业务系统进行解耦。即在request到达其请求的url之前﹐先对这个url和请求方进行权限验证﹐如果通过﹐我们就放它过去﹐什么都不做﹐如果不通过﹐我们就可以向客户端发送相关的拒绝信息﹐并不让web服务器真正执行到那个url﹐完成安全管控。 在web安全管控中﹐授权的除了要识别授权的客体(URL)之外﹐我们还必须识别授权的主体﹐即请求方的认定﹐也就是常说的认证机制。 由于http协议无状态的特点﹐每次request时﹐web服务器都无法识别这个请求是否和上次的请求是否相同。因此认证机制在某种程度上来说其实相当困难。 曾经遇到过通过IP来认证的﹐先不说这种机制对于web可以anywhere访问是一种倒退﹐单是那种IP更改﹐欺骗或通过Proxy访问就无法适用了。 现在最多的做法还是通过cookie和session来完成的。 不过最好还是清楚一下cookie和session的原理﹕ Cookie﹕cookie其实也是http request header的一部分﹐我们可以把任何值当作cookie发给web服务器。 至于Session,不知道大家有没有看过.net的session实现机制﹐每次请求后﹐.net会写入一个session_id的cookie到客户端﹐这样在下次客户再请求时﹐提取这个cookie来识别。剩下的就和cookie一样了。
曾经有人设计过这样一个系统﹐要我尝试攻入其中某个已管控的页面中。 它是这样做的﹐在每个要权限的aspx页面的page_load中判断Session["userid"]是否为null,如果不是﹐则转向登录页面。
在我截获了网络上某个已登录用户和web服务器通讯的request和response之后﹐提取其cookie信息﹐交将它放入我的request请求中﹐我就以那个登录用户的身份执行了那支程序了。
但是这并不是说就不能使用cookie和session来作为认证的机制﹐我的意思是﹐web应用程序的安全也是相对的﹐必须建立在基本的网络安全和用户安全防范意识之上。可以采取包括加密会关键页面(如登录页面)的会话(例如使用https)或要求用户每次使用完系统后注销或关闭浏览器﹐以及尽可能多的对cookie和session做更多验证等。
在认证和授权的原理讲完后﹐要在asp.net应用程序中要完成上述的安全管控其实非常简单﹐设计一个httpmodule﹐然后捕获相关的事件﹐在这个事件中进行权限判断即可。 1 /**//// <summary>
2 /// 使用HttpModule模組進行web權限管控 3 /// </summary> 4 /// <remarks> 5 /// 自定義一個HttpModule﹐并在AuthorizeRequest事件中完成授權動作 6 /// </remarks> 7 public class WebSecurityModule:IHttpModule 8 { 9 10 11 12 /**//// <summary> 13 /// 在AuthorizeRequest事件中,進行驗証和授權 14 /// </summary> 15 /// <param name="context"></param> 16 public void Init(HttpApplication context) 17 { 18context.AuthorizeRequest += new EventHandler(OnAuthorize); 19 } 20 21 /**//// <summary> 22 /// 調用PFSAuthorize類進行授權 23 /// </summary> 24 /// <param name="sender"></param> 25 /// <param name="e"></param> 26 /// <remarks>主要是看當前用戶(包括匿名用戶)是否擁有當前Request的url的權限</remarks> 27 public void OnAuthorize(Object sender,EventArgs e) 28 { 29 //認証﹕提取用戶ID 30 string userid = getuserid(); 31 //授權﹕判斷用戶ID是否有URL的權限 32 bool hasright = authroize(userid,HttpContext.Current.Request.Url); 33 if (!hasright) 34 { 35 //進行無權信息返回 36 //如轉向無權登錄頁面 37 Response.Redirect("error.aspx"); 38 } 39 } 40 } 最后我們只要將這個類封裝成一個單獨的DLL﹐然后在每個web.config的httpmodules節中配置即完成了安全管控 |
|