在關(guān)于web應用程序安全的思考(序)中我曾提到﹕web應用程序的安全不應該依賴(lài)于客戶(hù)端的請求信息。
眾所周知﹐http協(xié)議是開(kāi)放的﹐因此誰(shuí)都能向網(wǎng)絡(luò )上公開(kāi)的web服務(wù)器發(fā)送request請求﹐要求一個(gè)URL(Uniform Resource Locator 統一資源定位符)。
所謂request﹐不過(guò)是符合http協(xié)議(即遵守http請求語(yǔ)法)的一大段字符串而已﹕
下面是一個(gè)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
下面是一個(gè)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://tempuri.org/HelloWorld"
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><HelloWorld xmlns="http://tempuri.org/" /></soap:Body></soap:Envelope>
相信大家基本上能理解上述字符串的意義。這表明我們只要組織類(lèi)似的字符串﹐然后發(fā)往相應的web服務(wù)器﹐就可以請求到某個(gè)URL了﹐也就是說(shuō)web請求不依賴(lài)瀏覽器(其實(shí)web也不依賴(lài)服務(wù)器﹐它只依賴(lài)http協(xié)議)。
下面的這個(gè)程序是C#寫(xiě)的通過(guò)socket直接向web服務(wù)器發(fā)送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 //發(fā)送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("正在發(fā)送請求");
44 s.Send(bytesSent, bytesSent.Length, 0);
45 int bytes = 0;
46 StringBuilder responsestr = new StringBuilder();
47 Console.WriteLine("正在接收web服務(wù)器的回應");
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}
相關(guān)的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頁(yè)面:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="TestRequest.aspx.cs" Inherits="TestRequest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/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服務(wù)器對于請求方的識別能力是很低的。因此作為web應用程序安全管控的唯一依據就只能是request的url了﹐因為只有它才是真實(shí)的﹐而我們進(jìn)行安全管控的最終目的也就是
判斷這個(gè)請求方是否擁有這個(gè)url的權限(即授權)
這就是我抽象出來(lái)的web安全管控的本質(zhì)﹐依據這點(diǎn)﹐我們就可以把web安全管控和業(yè)務(wù)系統進(jìn)行解耦。即在request到達其請求的url之前﹐先對這個(gè)url和請求方進(jìn)行權限驗證﹐如果通過(guò)﹐我們就放它過(guò)去﹐什么都不做﹐如果不通過(guò)﹐我們就可以向客戶(hù)端發(fā)送相關(guān)的拒絕信息﹐并不讓web服務(wù)器真正執行到那個(gè)url﹐完成安全管控。
在web安全管控中﹐授權的除了要識別授權的客體(URL)之外﹐我們還必須識別授權的主體﹐即請求方的認定﹐也就是常說(shuō)的認證機制。
由于http協(xié)議無(wú)狀態(tài)的特點(diǎn)﹐每次request時(shí)﹐web服務(wù)器都無(wú)法識別這個(gè)請求是否和上次的請求是否相同。因此認證機制在某種程度上來(lái)說(shuō)其實(shí)相當困難。
曾經(jīng)遇到過(guò)通過(guò)IP來(lái)認證的﹐先不說(shuō)這種機制對于web可以anywhere訪(fǎng)問(wèn)是一種倒退﹐單是那種IP更改﹐欺騙或通過(guò)Proxy訪(fǎng)問(wèn)就無(wú)法適用了。
現在最多的做法還是通過(guò)cookie和session來(lái)完成的。
不過(guò)最好還是清楚一下cookie和session的原理﹕
Cookie﹕cookie其實(shí)也是http request header的一部分﹐我們可以把任何值當作cookie發(fā)給web服務(wù)器。
至于Session,不知道大家有沒(méi)有看過(guò).net的session實(shí)現機制﹐每次請求后﹐.net會(huì )寫(xiě)入一個(gè)session_id的cookie到客戶(hù)端﹐這樣在下次客戶(hù)再請求時(shí)﹐提取這個(gè)cookie來(lái)識別。剩下的就和cookie一樣了。
大家可以看一下我上面這個(gè)例子﹐在那支request請求的程式中加入相關(guān)的session_id的cookie﹐你會(huì )發(fā)現程式是無(wú)法識別是不是真正的session的
曾經(jīng)有人設計過(guò)這樣一個(gè)系統﹐要我嘗試攻入其中某個(gè)已管控的頁(yè)面中。
它是這樣做的﹐在每個(gè)要權限的aspx頁(yè)面的page_load中判斷Session["userid"]是否為null,如果不是﹐則轉向登錄頁(yè)面。
在我截獲了網(wǎng)絡(luò )上某個(gè)已登錄用戶(hù)和web服務(wù)器通訊的request和response之后﹐提取其cookie信息﹐交將它放入我的request請求中﹐我就以那個(gè)登錄用戶(hù)的身份執行了那支程序了。
但是這并不是說(shuō)就不能使用cookie和session來(lái)作為認證的機制﹐我的意思是﹐web應用程序的安全也是相對的﹐必須建立在基本的網(wǎng)絡(luò )安全和用戶(hù)安全防范意識之上??梢圆扇“用軙?huì )關(guān)鍵頁(yè)面(如登錄頁(yè)面)的會(huì )話(huà)(例如使用https)或要求用戶(hù)每次使用完系統后注銷(xiāo)或關(guān)閉瀏覽器﹐以及盡可能多的對cookie和session做更多驗證等。
在認證和授權的原理講完后﹐要在asp.net應用程序中要完成上述的安全管控其實(shí)非常簡(jiǎn)單﹐設計一個(gè)httpmodule﹐然后捕獲相關(guān)的事件﹐在這個(gè)事件中進(jìn)行權限判斷即可。
下面是一些框架代碼﹕
1 /**//// <summary>
2 /// 使用HttpModule模組進(jìn)行web權限管控
3 /// </summary>
4 /// <remarks>
5 /// 自定義一個(gè)HttpModule﹐并在A(yíng)uthorizeRequest事件中完成授權動(dòng)作
6 /// </remarks> 7 public class WebSecurityModule:IHttpModule
8 {
9
10
11
12 /**//// <summary>
13 /// 在A(yíng)uthorizeRequest事件中,進(jìn)行驗証和授權
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類(lèi)進(jìn)行授權
23 /// </summary>
24 /// <param name="sender"></param>
25 /// <param name="e"></param>
26 /// <remarks>主要是看當前用戶(hù)(包括匿名用戶(hù))是否擁有當前Request的url的權限</remarks>
27 public void OnAuthorize(Object sender,EventArgs e)
28 {
29 //認証﹕提取用戶(hù)ID
30 string userid = getuserid();
31 //授權﹕判斷用戶(hù)ID是否有URL的權限
32 bool hasright = authroize(userid,HttpContext.Current.Request.Url);
33 if (!hasright)
34 {
35 //進(jìn)行無(wú)權信息返回
36 //如轉向無(wú)權登錄頁(yè)面
37 Response.Redirect("error.aspx");
38 }
39 }
40 }
最后我們只要將這個(gè)類(lèi)封裝成一個(gè)單獨的DLL﹐然后在每個(gè)web.config的httpmodules節中配置即完成了安全管控
后面的文章我會(huì )講如何設計這些模塊達到最好擴展性