您的 Java 程式碼安全嗎?

herrapfel發表於2005-06-10

  雖然客戶仍然很關心您為他們構建的應用程式的可伸縮性和可用性,但他們可能變得也很關心安全性,而且要求特別嚴格。應用程式可能容易受到兩類安全性威脅的攻擊:靜態和動態。雖然開發人員不能完全控制動態威脅,但在開發應用程式時,您可以採取一些預防措施來消除靜態威脅。本文概括並解釋了 13 種型別的靜態暴露 ― 它們是系統中的缺陷,它使系統暴露在想要篡奪該系統的特權的攻擊者面前。您將學會如何處理這些暴露,以及如何發現(如果不處理這些暴露)這些暴露可能造成的影響。

  在開發 Java Web 應用程式時,您需要確保應用程式擁有完善的安全性特徵補充。這裡在談到 Java 安全性時,我們並不談及 Java 語言提供的安全性 API,也不涉及使用 Java 程式碼來保護應用程式。本文將著重討論可能潛伏在您的 Java 應用程式中的 安全性暴露。安全性暴露是系統中的缺陷,它使系統無法 ― 即使系統被正常使用 ― 防止攻擊者篡奪對系統的特權、控制系統的執行、危及系統上的資料安全或者假冒未經授權的信任。相對於安全性暴露,許多開發人員更加關心網站的感官效果。

  毫無疑問,客戶現在既嚴格地關注效能、可伸縮性和可用性也嚴格地關注安全性。應用程式可能容易受到兩類安全性威脅的攻擊: 動態和 靜態。動態威脅是那些同未經授權進入系統有關的威脅,或那些同跨越網路傳輸的資料的完整性、隱私和機密性有關的威脅。這些威脅同應用程式的功能程式碼沒有多大關係;使用加密、加密術和認證技術來消除這些威脅。相比之下,靜態威脅卻同應用程式的功能程式碼 有關;它們同進入系統的授權使用者所做的事情有關。未知使用者闖入系統是動態威脅的一個示例;授權使用者以未授權方式作業系統內的程式碼或資料是靜態威脅的示例。應用程式開發人員並不能完全控制動態威脅;但開發人員在構建應用程式時卻可以採取預防措施來消除靜態威脅。

  在本文中,我們討論了對付 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+"/n"+"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 程式碼。這些建議並不能解決您的所有安全性問題,但它們將減少暴露數目。最佳軟體安全性實踐可以幫助確保軟體正常執行。安全至關重要和高可靠系統設計者總是花費大量精力來分析和跟蹤軟體行為。只有通過將安全性作為至關緊要的系統特性來對待 ― 並且從一開始就將它構建到應用程式中,我們才可以避免亡羊補牢似的、修修補補的安全性方法。

相關文章