在本文中,我們討論了對付 13 種不同靜態暴露的技巧。對於每種暴露,我們解釋了不處理這些安全性問題所造成的影響。我們還為您推薦了一些準則,要開發不受這些靜態安全性暴露威脅的、健壯且安全的 Java 應用程式,您應該遵循這些準則。一有合適的時機,我們就提供代碼樣本(既有暴露的代碼也有無暴露的代碼)。
對付高嚴重性暴露的技巧
請遵循下列建議以避免高嚴重性靜態安全性暴露:
限制對變數的訪問
讓每個類和方法都成為 final,除非有足夠的理由不這樣做
不要依賴包範圍
使類不可複製
使類不可序列化
使類無法復原序列化
避免寫入程式碼敏感性資料
尋找惡意代碼
限制對變數的訪問
如果將變數聲明為 public,那麼外部代碼就可以操作該變數。這可能會導致安全性暴露。
影響
如果執行個體變數為 public,那麼就可以在類執行個體上直接存取和操作該執行個體變數。將執行個體變數聲明為 protected 並不一定能解決這一問題:雖然不可能直接在類執行個體基礎上訪問這樣的變數,但仍然可以從衍生類別訪問這個變數。
清單 1 示範了帶有 public 變數的代碼,因為變數為 public 的,所以它暴露了。
清單 1. 帶有 public 變數的代碼
class Test {
public int id;
protected String name;
Test(){
id = 1;
name = "hello world";
}
//code
}
public class MyClass extends Test{
public void methodIllegalSet(String name){
this.name = name; // this should not be allowed
}
public static void main(String[] args){
Test obj = new Test();
obj.id = 123; // this should not be allowed
MyClass mc = new MyClass();
mc.methodIllegalSet("Illegal Set Value");
}
}
建議
一般來說,應該使用取值方法而不是 public 變數。按照具體問題具體對待的原則,在確定哪些變數特別重要因而應該聲明為 private 時,請將編碼的方便程度及成本同安全性需要加以比較。清單 2 示範了以下列方式來使之安全的代碼:
清單 2. 不帶有 public 變數的代碼
class Test {
private int id;
private String name;
Test(){
id = 1;
name = "hello world";
}
public void setId(int id){
this.id = id;
}
public void setName(String name){
this.name = name;
}
public int getId(){
return id;
}
public String getName(){
return name;
}
}
讓每個類和方法都為 final
不允許擴充的類和方法應該聲明為 final。這樣做防止了系統外的代碼擴充類並修改類的行為。
影響
僅僅將類聲明為非 public 並不能防止攻擊者擴充類,因為仍然可以從它自己的包內訪問該類。
建議
讓每個類和方法都成為 final,除非有足夠的理由不這樣做。按此建議,我們要求您放棄可擴充性,雖然它是使用諸如 Java 語言之類的物件導向語言的主要優點之一。在試圖提供安全性時,可擴充性卻成了您的敵人;可擴充性只會為攻擊者提供更多給您帶來麻煩的方法。
不要依賴包範圍
沒有顯式地標註為 public、private 或 protected 的類、方法和變數在它們自己的包內是可訪問的。
影響
如果 Java 包不是封閉的,那麼攻擊者就可以向包內引入新類並使用該新類來訪問您想保護的內容。諸如 java.lang 之類的一些包預設是封閉的,一些 JVM 也讓您封閉自己的包。然而,您最好假定包是不封閉的。
建議
從軟體工程觀點來看,包範圍具有重要意義,因為它可以阻止對您想隱藏的內容進行偶然的、無意中的訪問。但不要依靠它來擷取安全性。應該將類、方法和變數顯式標註為 public、private 或 protected 中適合您特定需求的那種。
使類不可複製
複製允許繞過構造器而輕易地複製類執行個體。
影響
即使您沒有有意使類可複製,外部源仍然可以定義您的類的子類,並使該子類實現 java.lang.Cloneable。這就讓攻擊者建立了您的類的新執行個體。拷貝現有對象的記憶體映象產生了新的執行個體;雖然這樣做有時候是產生新對象的可接受方法,但是大多數時候是不可接受的。清單 3 說明了因為可複製而暴露的代碼:
清單 3. 可複製代碼
class MyClass{
private int id;
private String name;
public MyClass(){
id=1;
name="HaryPorter";
}
public MyClass(int id,String name){
this.id=id;
this.name=name;
}
public void display(){
System.out.println("Id ="+id+"
"+"Name="+name);
}
}
// hackers code to clone the user class
public class Hacker extends MyClass implements Cloneable {
public static void main(String[] args){
Hacker hack=new Hacker();
try{
MyClass o=(MyClass)hack.clone();
o.display();
}
catch(CloneNotSupportedException e){
e.printStackTrace();
}
}
}
建議
要防止類被複製,可以將清單 4 中所示的方法添加到您的類中:
清單 4. 使您的代碼不可複製
public final Object clone()
throws java.lang.CloneNotSupportedException{
throw new java.lang.CloneNotSupportedException();
}
如果想讓您的類可複製並且您已經考慮了這一選擇的後果,那麼您仍然可以保護您的類。要做到這一點,請在您的類中定義一個為 final 的複製方法,並讓它依賴於您的一個超類中的一個非 final 複製方法,如清單 5 中所示:
清單 5. 以安全的方式使您的代碼可複製
public final Object clone()
throws java.lang.CloneNotSupportedException {
super.clone();
}
類中出現 clone() 方法防止攻擊者重新定義您的 clone 方法。
使類不可序列化
序列化允許將類執行個體中的資料儲存在外部檔案中。闖入代碼可以複製或複製執行個體,然後對它進行序列化。
影響
序列化是令人擔憂的,因為它允許外部源擷取對您的對象的內部狀態的控制。這一外部源可以將您的對象之一序列化成攻擊者隨後可以讀取的位元組數組,這使得攻擊者可以完全審查您的對象的內部狀態,包括您標記為 private 的任何欄位。它也允許攻擊者訪問您引用的任何對象的內部狀態。
建議
要防止類中的對象被序列化,請在類中定義清單 6 中的 writeObject() 方法:
清單 6. 防止對象序列化
private final void writeObject(ObjectOutputStream out)
throws java.io.NotSerializableException {
throw new java.io.NotSerializableException("This object cannot
be serialized");
}
通過將 writeObject() 方法聲明為 final,防止了攻擊者覆蓋該方法。
使類無法復原序列化
通過使用逆序列化,攻擊者可以用外部資料或位元組流來執行個體化類。
影響
不管類是否可以序列化,都可以對它進行逆序列化。外部源可以建立逆序列化成類執行個體的位元組序列。這種可能為您帶來了大量風險,因為您不能控制逆序列化對象的狀態。請將逆序列化作為您的對象的另一種公用構造器 ? 一種您無法控制的構造器。
建議
要防止對對象的逆序列化,應該在您的類中定義清單 7 中的 readObject() 方法:
清單 7. 防止對象逆序列化
private final void readObject(ObjectInputStream in)
throws java.io.NotSerializableException {
throw new java.io.NotSerializableException("This object cannot
be deserialized");
}
通過將該方法聲明為 final,防止了攻擊者覆蓋該方法。
避免寫入程式碼敏感性資料
您可能會嘗試將諸如加密金鑰之類的秘密存放在您的應用程式或庫的代碼。對於你們開發人員來說,這樣做通常會把事情變得更簡單。
影響
任何運行您的代碼的人都可以完全訪問以這種方法儲存的秘密。沒有什麼東西可以防止心懷叵測的程式員或虛擬機器窺探您的代碼並瞭解其秘密。
建議
可以以一種只可被您解密的方式將秘密儲存在您代碼中。在這種情形下,秘密只在於您的代碼所使用的演算法。這樣做沒有多大壞處,但不要洋洋得意,認為這樣做提供了牢固的保護。您可以遮掩您的原始碼或位元組碼 ? 也就是,以一種為瞭解密必須知道加密格式的方法對原始碼或位元組碼進行加密 ? 但攻擊者極有可能能夠推斷出加密格式,對遮掩的代碼進行逆向工程從而揭露其秘密。
這一問題的一種可能解決方案是:將敏感性資料儲存在屬性檔案中,無論什麼時候需要這些資料,都可以從該檔案讀取。如果資料極其敏感,那麼在訪問屬性檔案時,您的應用程式應該使用一些加密/解密技術。
尋找惡意代碼
從事某個項目的某個心懷叵測的開發人員可能故意引入易受攻擊的代碼,打算日後利用它。這樣的代碼在初始化時可能會啟動一個後台進程,該進程可以為闖入者開後門。它也可以更改一些敏感性資料。
這樣的惡意代碼有三類:
類中的 main 方法
定義過且未使用的方法
注釋中的無作用程式碼
影響
進入點程式可能很危險而且有惡意。通常,Java 開發人員往往在其類中編寫 main() 方法,這有助於測試單個類的功能。當類從測試轉移到生產環境時,帶有 main() 方法的類就成為了對應用程式的潛在威脅,因為闖入者將它們用作進入點。
請檢查代碼中是否有未使用的方法出現。這些方法在測試期間將會通過所有的安全檢查,因為在代碼中不調用它們 ? 但它們可能含有寫入程式碼在它們內部的敏感性資料(雖然是測試資料)。引入一小段代碼的攻擊者隨後可能調用這樣的方法。
避免最終應用程式中的無作用程式碼(注釋內的代碼)。如果闖入者去掉了對這樣的代碼的注釋,那麼代碼可能會影響系統的功能性。
可以在清單 8 中看到所有三種類型的惡意代碼的樣本:
清單 8. 潛在惡意的 Java 代碼
public void unusedMethod(){
// code written to harm the system
}
public void usedMethod(){
//unusedMethod(); //code in comment put with bad intentions,
//might affect the system if uncommented
// int x = 100;
// x=x+10; //Code in comment, might affect the
//functionality of the system if uncommented
}
建議
應該將(除啟動應用程式的 main() 方法之外的)main() 方法、未使用的方法以及無作用程式碼從應用程式代碼中除去。在軟體交付使用之前,主要開發人員應該對敏感應用程式進行一次全面的程式碼檢閱。應該使用“Stub”或“dummy”類代替 main() 方法以測試應用程式的功能。
對付中等嚴重性暴露的技巧
請遵循下列建議以避免中等嚴重性靜態安全性暴露:
不要依賴初始化
不要通過名稱來比較類
不要使用內部類
不要依賴初始化
您可以不運行構造器而指派至。這些對象使用起來不安全,因為它們不是通過構造器初始化的。
影響
在初始化時驗證對象確保了資料的完整性。
例如,請想象為客戶建立新帳戶的 Account 對象。只有在 Account 期初餘額大於 0 時,才可以開設新帳戶。可以在構造器裡執行這樣的驗證。有些人未執行構造器而建立 Account 對象,他可能建立了一個具有一些負值的新帳戶,這樣會使系統不一致,容易受到進一步的幹預。
建議
在使用對象之前,請檢查對象的初始化過程。要做到這一點,每個類都應該有一個在構造器中設定的私人布爾標誌,如清單 9 中的類所示。在每個非 static 方法中,代碼在任何進一步執行之前都應該檢查該標誌的值。如果該標誌的值為 true,那麼控制應該進一步繼續;否則,控制應該拋出一個例外並停止執行。那些從構造器調用的方法將不會檢查初始化的變數,因為在調用方法時沒有設定標誌。因為這些方法並不檢查標誌,所以應該將它們聲明為 private 以防止使用者直接存取它們。
清單 9. 使用布爾標誌以檢查初始化過程
public class MyClass{
private boolean initialized = false;
//Other variables
public MyClass (){
//variable initialization
method1();
initialized = true;
}
private void method1(){ //no need to check for initialization variable
//code
}
public void method2(){
try{
if(initialized==true){
//proceed with the business logic
}
else{
throw new Exception("Illegal State Of the object");
}
}catch(Exception e){
e.printStackTrace();
}
}
}
如果對象由逆序列化進行初始化,那麼上面討論的驗證機制將難以奏效,因為在該過程中並不調用構造器。在這種情況下,類應該實現 ObjectInputValidation 介面:
清單 10. 實現 ObjectInputValidation
interface java.io.ObjectInputValidation {
public void validateObject() throws InvalidObjectException;
}
所有驗證都應該在 validateObject() 方法中執行。對象還必須調用 ObjectInputStream.RegisterValidation() 方法以為逆序列化對象之後的驗證進行註冊。 RegisterValidation() 的第一個參數是實現 validateObject() 的對象,通常是對對象自身的引用。註:任何實現 validateObject() 的對象都可能充當對象驗證器,但對象通常驗證它自己對其它對象的引用。RegisterValidation() 的第二個參數是一個確定回調順序的整數優先順序,優先順序數字大的比優先順序數字小的先回調。同一優先順序內的回調順序則不確定。
當對象已逆序列化時,ObjectInputStream 按照從高到低的優先順序順序調用每個登入對象上的 validateObject()。
不要通過名稱來比較類
有時候,您可能需要比較兩個對象的類,以確定它們是否相同;或者,您可能想看看某個對象是否是某個特定類的執行個體。因為 JVM 可能包括多個具有相同名稱的類(具有相同名稱但卻在不同包內的類),所以您不應該根據名稱來比較類。
影響
如果根據名稱來比較類,您可能無意中將您不希望授予別人的權利授予了闖入者的類,因為闖入者可以定義與您的類同名的類。
例如,請假設您想確定某個對象是否是類 com.bar.Foo 的執行個體。清單 11 示範了完成這一任務的錯誤方法:
清單 11. 比較類的錯誤方法
if(obj.getClass().getName().equals("Foo")) // Wrong!
// objects class is named Foo
}else{
// object's class has some other name
}
建議
在那些非得根據名稱來比較類的情況下,您必須格外小心,必須確保使用了當前類的 ClassLoader 的當前名稱空間,如清單 12 中所示:
清單 12. 比較類的更好方法
if(obj.getClass() == this.getClassLoader().loadClass("com.bar.Foo")){
// object's class is equal to
//the class that this class calls "com.bar.Foo"
}else{
// object's class is not equal to the class that
// this class calls "com.bar.Foo"
}
然而,比較類的更好方法是直接比較類對象看它們是否相等。例如,如果您想確定兩個對象 a 和 b 是否屬同一個類,那麼您就應該使用清單 13 中的代碼:
清單 13. 直接比較對象來看它們是否相等
if(a.getClass() == b.getClass()){
// objects have the same class
}else{
// objects have different classes
}
儘可能少用直接名稱比較。
不要使用內部類
Java 位元組碼沒有內部類的概念,因為編譯器將內部類轉換成了普通類,而如果沒有將內部類聲明為 private,則同一個包內的任何代碼恰好能訪問該普通類。
影響
因為有這一特性,所以包內的惡意代碼可以訪問這些內部類。如果內部類能夠訪問括起外部類的欄位,那麼情況會變得更糟。可能已經將這些欄位聲明為 private,這樣內部類就被轉換成了獨立類,但當內部類訪問外部類的欄位時,編譯器就將這些欄位從專用(private)的變為在包(package)的範圍內有效。內部類暴露了已經夠糟糕的了,但更糟糕的是編譯器使您將某些欄位成為 private 的舉動成為徒勞。
建議 如果能夠不使用內部類就不要使用內部類。
對付低嚴重性暴露的技巧
請遵循下列建議以避免低嚴重性靜態安全性暴露:
避免返回可變對象
檢查本機方法
避免返回可變對象
Java 方法返回對象引用的副本。如果實際對象是可改變的,那麼使用這樣一個引用調用程式可能會改變它的內容,通常這是我們所不希望見到的。
影響
請考慮這個樣本:某個方法返回一個對敏感對象的內部數組的引用,假定該方法的調用程式不改變這些對象。即使數組對象本身是不可改變的,也可以在數組對象以外運算元組的內容,這種操作將反映在返回該數組的對象中。如果該方法返回可改變的對象,那麼事情會變得更糟;外部實體可以改變在那個類中聲明的 public 變數,這種改變將反映在實際對象中。
清單 14 示範了脆弱性。getExposedObj() 方法返回了 Exposed 對象的引用副本,該對象是可變的:
清單 14. 返回可變對象的引用副本
class Exposed{
private int id;
private String name;
public Exposed(){
}
public Exposed(int id, String name){
this.id = id;
this.name = name;
}
public int getId(){
return id;
}
public String getName(){
return name;
}
public void setId(int id){
this.id=id;
}
public void setName(String name){
this.name = name;
}
public void display(){
System.out.println("Id = "+ id + " Name = "+ name);
}
}
public class Exp12{
private Exposed exposedObj = new Exposed(1,"Harry Porter");
public Exposed getExposedObj(){
return exposedObj; //returns a reference to the object.
}
public static void main(String[] args){
Exp12 exp12 = new Exp12();
exp12.getExposedObj().display();
Exposed exposed = exp12.getExposedObj();
exposed.setId(10);
exposed.setName("Hacker");
exp12.getExposedObj().display();
}
}
建議
如果方法返回可改變的對象,但又不希望調用程式改變該對象,請修改該方法使之不返回實際對象而是返回它的副本或複製。要改正清單 14 中的代碼,請讓它返回 Exposed 對象的副本,如清單 15 中所示:
清單 15. 返回可變對象的副本
public Exposed getExposedObj(){
return new Exposed(exposedObj.getId(),exposedObj.getName());
}
或者,您的代碼也可以返回 Exposed 對象的複製。
檢查本機方法
本機方法是一種 Java 方法,其實現是用另一種程式設計語言編寫的,如 C 或 C++。有些開發人員實現本機方法,這是因為 Java 語言即使使用即時(just-in-time)編譯器也比許多編譯過的語言要慢。其它人需要使用機器碼是為了在 JVM 以外實現特定於平台的功能。
影響
使用機器碼時,請小心,因為對這些代碼進行驗證是不可能的,而且機器碼可能潛在地允許 applet 繞過通常的安全性管理器(Security Manager)和 Java 對裝置訪問的控制。
建議
如果非得使用本機方法,那麼請檢查這些方法以確定:
它們返回什麼
它們擷取什麼作為參數
它們是否繞過安全性檢查
它們是否是 public、private 等等
它們是否含有繞過包邊界從而繞過包保護的方法調用
結束語
編寫安全 Java 代碼是十分困難的,但本文描述了一些可行的實踐來幫您編寫安全 Java 代碼。這些建議並不能解決您的所有安全性問題,但它們將減少暴露數目。最佳軟體安全性實踐可以協助確保軟體正常運行。安全至關重要和高可靠系統設計者總是花費大量精力來分析和跟蹤軟體行為。只有通過將安全性作為至關緊要的系統特性來對待 ? 並且從一開始就將它構建到應用程式中,我們才可以避免亡羊補牢似的、修修補補的安全性方法。
參考資料
請通過單擊文章頂部或底部的討論來參加本文的論壇。
瞭解關於 Java 安全性 API 的更多知識。
developerWorks 安全專題上通常含有有關電腦安全性的優秀資源。
Larry Koved、 Anthony J. Nadalin、Don Neal 和 Tim Lawson 合作編寫的 “The evolution of Java security”(developerWorks,1998 年)對 Java 語言的安全性模型早期開發進行了深入探討。
Sing Li 在他的 Java 安全性系列文章(由兩部分組成的)(developerWorks, 2001 年 2 月)中向開發人員顯示:儘管社區可能不得不重新考慮 Java 2 中的安全性設計,還是出現了只對開發人員有協助,可以滿足他們的需求的一致的進展:
第一部分
第二部分
John Viega、Tom Mutdosch、 Gary McGraw 和 Ed Felten 合著的 “Statically scanning Java code for security vulnerabilities” (IEEE Software,2000 年 9 月)介紹了一種 Java 工具,可以使用該工具來檢查您的 Java 代碼中的安全性漏洞。
G. McGraw 和 E. Felten 合作編寫的 Securing Java: Getting Down to Business with Mobile Code(John Wiley 和 Sons,1998 年)深入涵蓋了 Java 安全性。(文檔是 PDF 格式的。)
定期檢查 IBM 研究 Java 安全頁面以便 IBM 在安全性領域的創新有重要發展時能夠跟蹤這一創新。
如果您的 Java 代碼運行在 S/390 系統上,那麼您將需要查閱 S/390 Java 安全頁面以擷取額外的資訊。
關於作者
Bijaya Nanda Sahu 是就職於印度 IBM Global Services 的軟體工程師。他從事過各種網際網路技術和架構(J2EE、WSBCC、JADE)、 WebSphere 相關技術、UML 和 OOAD 方面的工作。目前,他從事網際網路銀行安全性問題方面的工作,重點在 WebSphere Application Server 和 Portal Server 上。可以通過 bijaya.sahu@in.ibm.com 和他聯絡