雖然客戶仍然很關心您為他們構建的應用程序的可伸縮性和可用性但他們可能變得也很關心安全性而且要求特別嚴格應用程序可能容易受到兩類安全性威脅的攻擊靜態和動態雖然開發人員不能完全控制動態威脅但在開發應用程序時您可以采取一些預防措施來消除靜態威脅本文概括並解釋了 種類型的靜態暴露 ― 它們是系統中的缺陷它使系統暴露在想要篡奪該系統的特權的攻擊者面前您將學會如何處理這些暴露以及如何發現(如果不處理這些暴露)這些暴露可能造成的影響
在開發 Java Web 應用程序時您需要確保應用程序擁有完善的安全性特征補充這裡在談到 Java 安全性時我們並不談及 Java 語言提供的安全性 API也不涉及使用 Java 代碼來保護應用程序本文將著重討論可能潛伏在您的 Java 應用程序中的 安全性暴露安全性暴露是系統中的缺陷它使系統無法 ― 即使系統被正常使用 ― 防止攻擊者篡奪對系統的特權控制系統的運行危及系統上的數據安全或者假冒未經授權的信任相對於安全性暴露許多開發人員更加關心網站的感官效果
毫無疑問客戶現在既嚴格地關注性能可伸縮性和可用性也嚴格地關注安全性應用程序可能容易受到兩類安全性威脅的攻擊 動態和 靜態動態威脅是那些同未經授權進入系統有關的威脅或那些同跨越網絡傳輸的數據的完整性隱私和機密性有關的威脅這些威脅同應用程序的功能代碼沒有多大關系使用加密加密術和認證技術來消除這些威脅相比之下靜態威脅卻同應用程序的功能代碼 有關它們同進入系統的授權用戶所做的事情有關未知用戶闖入系統是動態威脅的一個示例授權用戶以未授權方式操作系統內的代碼或數據是靜態威脅的示例應用程序開發人員並不能完全控制動態威脅但開發人員在構建應用程序時卻可以采取預防措施來消除靜態威脅
在本文中我們討論了對付 種不同靜態暴露的技巧對於每種暴露我們解釋了不處理這些安全性問題所造成的影響我們還為您推薦了一些准則要開發不受這些靜態安全性暴露威脅的健壯且安全的 Java 應用程序您應該遵循這些准則一有合適的時機我們就提供代碼樣本(既有暴露的代碼也有無暴露的代碼)
對付高嚴重性暴露的技巧
請遵循下列建議以避免高嚴重性靜態安全性暴露
·限制對變量的訪問
·讓每個類和方法都成為 final除非有足夠的理由不這樣做
·不要依賴包作用域
·使類不可克隆
·使類不可序列化
·使類不可逆序列化
·避免硬編碼敏感數據
·查找惡意代碼
限制對變量的訪問
如果將變量聲明為 public那麼外部代碼就可以操作該變量這可能會導致安全性暴露
影響
如果實例變量為 public 那麼就可以在類實例上直接訪問和操作該實例變量將實例變量聲明為 protected 並不一定能解決這一問題雖然不可能直接在類實例基礎上訪問這樣的變量但仍然可以從派生類訪問這個變量
清單 演示了帶有 public 變量的代碼因為變量為 public 的所以它暴露了
清單 帶有 public 變量的代碼
Java代碼
class Test {
public int id;
protected String name;
Test(){
id = ;
name = hello world;
}
//code
}
public class MyClass extends Test{
public void methodIllegalSet(String name){
thisname = name; // this should not be allowed
}
public static void main(String[] args){
Test obj = new Test();
objid = ; // this should not be allowed
MyClass mc = new MyClass();
thodIllegalSet(Illegal Set Value);
}
}
建議
一般來說應該使用取值方法而不是 public 變量按照具體問題具體對待的原則在確定哪些變量特別重要因而應該聲明為 private 時請將編碼的方便程度及成本同安全性需要加以比較清單 演示了以下列方式來使之安全的代碼
清單 不帶有 public 變量的代碼
Java代碼
class Test {
private int id;
private String name;
Test(){
id = ;
name = hello world;
}
public void setId(int id){
thisid = id;
}
public void setName(String name){
thisname = name;
}
public int getId(){
return id;
}
public String getName(){
return name;
}
}
讓每個類和方法都為 final
不允許擴展的類和方法應該聲明為 final 這樣做防止了系統外的代碼擴展類並修改類的行為
影響
僅僅將類聲明為非 public 並不能防止攻擊者擴展類因為仍然可以從它自己的包內訪問該類
建議
讓每個類和方法都成為 final除非有足夠的理由不這樣做按此建議我們要求您放棄可擴展性雖然它是使用諸如 Java 語言之類的面向對象語言的主要優點之一在試圖提供安全性時可擴展性卻成了您的敵人可擴展性只會為攻擊者提供更多給您帶來麻煩的方法
不要依賴包作用域
沒有顯式地標注為 public private 或 protected 的類方法和變量在它們自己的包內是可訪問的
影響
如果 Java 包不是封閉的那麼攻擊者就可以向包內引入新類並使用該新類來訪問您想保護的內容諸如 javalang 之類的一些包缺省是封閉的一些 JVM 也讓您封閉自己的包然而您最好假定包是不封閉的
建議
從軟件工程觀點來看包作用域具有重要意義因為它可以阻止對您想隱藏的內容進行偶然的無意中的訪問但不要依靠它來獲取安全性應該將類方法和變量顯式標注為 public private 或 protected 中適合您特定需求的那種
使類不可克隆
克隆允許繞過構造器而輕易地復制類實例
影響
即使您沒有有意使類可克隆外部源仍然可以定義您的類的子類並使該子類實現 javalangCloneable 這就讓攻擊者創建了您的類的新實例拷貝現有對象的內存映象生成了新的實例雖然這樣做有時候是生成新對象的可接受方法但是大多數時候是不可接受的清單 說明了因為可克隆而暴露的代碼
清單 可克隆代碼
Java代碼
class MyClass{
private int id;
private String name;
public MyClass(){
id=;
name=HaryPorter;
}
public MyClass(int idString name){
thisid=id;
thisname=name;
}
public void display(){
Systemoutprintln(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)hackclone();
odisplay();
}
catch(CloneNotSupportedException e){
eprintStackTrace();
}
}
}
建議
要防止類被克隆可以將清單 中所示的方法添加到您的類中
清單 使您的代碼不可克隆
Java代碼
public final Object clone()
throws javalangCloneNotSupportedException{
throw new javalangCloneNotSupportedException();
}
如果想讓您的類可克隆並且您已經考慮了這一選擇的後果那麼您仍然可以保護您的類要做到這一點請在您的類中定義一個為 final 的克隆方法並讓它依賴於您的一個超類中的一個非 final 克隆方法如清單 中所示
清單 以安全的方式使您的代碼可克隆
Java代碼
public final Object clone()
throws javalangCloneNotSupportedException {
superclone();
}
類中出現 clone() 方法防止攻擊者重新定義您的 clone 方法
使類不可序列化
序列化允許將類實例中的數據保存在外部文件中闖入代碼可以克隆或復制實例然後對它進行序列化
影響
序列化是令人擔憂的因為它允許外部源獲取對您的對象的內部狀態的控制這一外部源可以將您的對象之一序列化成攻擊者隨後可以讀取的字節數組這使得攻擊者可以完全審查您的對象的內部狀態包括您標記為 private 的任何字段它也允許攻擊者訪問您引用的任何對象的內部狀態
建議
要防止類中的對象被序列化請在類中定義清單 中的 writeObject() 方法
清單 防止對象序列化
Java代碼
private final void writeObject(ObjectOutputStream out)
throws javaioNotSerializableException {
throw new javaioNotSerializableException(This object cannot be serialized);
}
通過將 writeObject() 方法聲明為 final防止了攻擊者覆蓋該方法
使類不可逆序列化
通過使用逆序列化攻擊者可以用外部數據或字節流來實例化類
影響
不管類是否可以序列化都可以對它進行逆序列化外部源可以創建逆序列化成類實例的字節序列這種可能為您帶來了大量風險因為您不能控制逆序列化對象的狀態請將逆序列化作為您的對象的另一種公共構造器 ― 一種您無法控制的構造器
建議
要防止對對象的逆序列化應該在您的類中定義清單 中的 readObject() 方法
清單 防止對象逆序列化
Java代碼
private final void readObject(ObjectInputStream in)
throws javaioNotSerializableException {
throw new javaioNotSerializableException(This object cannot be deserialized);
}
通過將該方法聲明為 final 防止了攻擊者覆蓋該方法
避免硬編碼敏感數據
您可能會嘗試將諸如加密密鑰之類的秘密存放在您的應用程序或庫的代碼對於你們開發人員來說這樣做通常會把事情變得更簡單
影響
任何運行您的代碼的人都可以完全訪問以這種方法存儲的秘密沒有什麼東西可以防止心懷叵測的程序員或虛擬機窺探您的代碼並了解其秘密
建議
可以以一種只可被您解密的方式將秘密存儲在您代碼中在這種情形下秘密只在於您的代碼所使用的算法這樣做沒有多大壞處但不要洋洋得意認為這樣做提供了牢固的保護您可以 遮掩您的源代碼或字節碼 ― 也就是以一種為了解密必須知道加密格式的方法對源代碼或字節碼進行加密 ― 但攻擊者極有可能能夠推斷出加密格式對遮掩的代碼進行逆向工程從而揭露其秘密
這一問題的一種可能解決方案是將敏感數據保存在屬性文件中無論什麼時候需要這些數據都可以從該文件讀取如果數據極其敏感那麼在訪問屬性文件時您的應用程序應該使用一些加密/解密技術
查找惡意代碼
從事某個項目的某個心懷叵測的開發人員可能故意引入易受攻擊的代碼打算日後利用它這樣的代碼在初始化時可能會啟動一個後台進程該進程可以為闖入者開後門它也可以更改一些敏感數據
這樣的惡意代碼有三類
·類中的 main 方法
·定義過且未使用的方法
·注釋中的死代碼
影響
入口點程序可能很危險而且有惡意通常Java 開發人員往往在其類中編寫 main() 方法這有助於測試單個類的功能當類從測試轉移到生產環境時帶有 main() 方法的類就成為了對應用程序的潛在威脅因為闖入者將它們用作入口點
請檢查代碼中是否有未使用的方法出現這些方法在測試期間將會通過所有的安全檢查因為在代碼中不調用它們 ― 但它們可能含有硬編碼在它們內部的敏感數據(雖然是測試數據)引入一小段代碼的攻擊者隨後可能調用這樣的方法
避免最終應用程序中的死代碼(注釋內的代碼)如果闖入者去掉了對這樣的代碼的注釋那麼代碼可能會影響系統的功能性
可以在清單 中看到所有三種類型的惡意代碼的示例
清單 潛在惡意的 Java 代碼
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 = ;
// x=x+; //Code in comment might affect the
//functionality of the system if uncommented
}
建議
應該將(除啟動應用程序的 main() 方法之外的) main() 方法未使用的方法以及死代碼從應用程序代碼中除去在軟件交付使用之前主要開發人員應該對敏感應用程序進行一次全面的代碼評審應該使用Stub或dummy類代替 main() 方法以測試應用程序的功能
對付中等嚴重性暴露的技巧
請遵循下列建議以避免中等嚴重性靜態安全性暴露
·不要依賴初始化
·不要通過名稱來比較類
·不要使用內部類
不要依賴初始化
您可以不運行構造器而分配對象這些對象使用起來不安全因為它們不是通過構造器初始化的
影響
在初始化時驗證對象確保了數據的完整性
例如請想象為客戶創建新帳戶的 Account 對象只有在 Account 期初余額大於 時才可以開設新帳戶可以在構造器裡執行這樣的驗證有些人未執行構造器而創建 Account 對象他可能創建了一個具有一些負值的新帳戶這樣會使系統不一致容易受到進一步的干預
建議
在使用對象之前請檢查對象的初始化過程要做到這一點每個類都應該有一個在構造器中設置的私有布爾標志如清單 中的類所示在每個非 static 方法中代碼在任何進一步執行之前都應該檢查該標志的值如果該標志的值為 true 那麼控制應該進一步繼續否則控制應該拋出一個例外並停止執行那些從構造器調用的方法將不會檢查初始化的變量因為在調用方法時沒有設置標志因為這些方法並不檢查標志所以應該將它們聲明為 private 以防止用戶直接訪問它們
清單 使用布爾標志以檢查初始化過程
Java代碼
public class MyClass{
private boolean initialized = false;
//Other variables
public MyClass (){
//variable initialization
method();
initialized = true;
}
private void method(){ //no need to check for initialization variable
//code
}
public void method(){
try{
if(initialized==true){
//proceed with the business logic
}
else{
throw new Exception(Illegal State Of the object);
}
}catch(Exception e){
eprintStackTrace();
}
}
}
如果對象由逆序列化進行初始化那麼上面討論的驗證機制將難以奏效因為在該過程中並不調用構造器在這種情況下類應該實現 ObjectInputValidation 接口
清單 實現 ObjectInputValidation
Java代碼
interface javaioObjectInputValidation {
public void validateObject() throws InvalidObjectException;
}
所有驗證都應該在 validateObject() 方法中執行對象還必須調用 ObjectInputStreamRegisterValidation() 方法以為逆序列化對象之後的驗證進行注冊 RegisterValidation() 的第一個參數是實現 validateObject() 的對象通常是對對象自身的引用注任何實現 validateObject() 的對象都可能充當對象驗證器但對象通常驗證它自己對其它對象的引用 RegisterValidation() 的第二個參數是一個確定回調順序的整數優先級優先級數字大的比優先級數字小的先回調同一優先級內的回調順序則不確定
當對象已逆序列化時 ObjectInputStream 按照從高到低的優先級順序調用每個已注冊對象上的 validateObject()
不要通過名稱來比較類
有時候您可能需要比較兩個對象的類以確定它們是否相同或者您可能想看看某個對象是否是某個特定類的實例因為 JVM 可能包括多個具有相同名稱的類(具有相同名稱但卻在不同包內的類)所以您不應該根據名稱來比較類
影響
如果根據名稱來比較類您可能無意中將您不希望授予別人的權利授予了闖入者的類因為闖入者可以定義與您的類同名的類
例如請假設您想確定某個對象是否是類 combarFoo 的實例清單 演示了完成這一任務的錯誤方法
清單 比較類的錯誤方法
Java代碼
if(objgetClass()getName()equals(Foo)) // Wrong!
// objects class is named Foo
}else{
// objects class has some other name
}
建議
在那些非得根據名稱來比較類的情況下您必須格外小心必須確保使用了當前類的 ClassLoader 的當前名稱空間如清單 中所示
清單 比較類的更好方法
Java代碼
if(objgetClass() == thisgetClassLoader()loadClass(combarFoo)){
// objects class is equal to
//the class that this class calls combarFoo
}else{
// objects class is not equal to the class that
// this class calls combarFoo
}
然而比較類的更好方法是直接比較類對象看它們是否相等例如如果您想確定兩個對象 a 和 b 是否屬同一個類那麼您就應該使用清單 中的代碼
清單 直接比較對象來看它們是否相等
Java代碼
if(agetClass() == bgetClass()){
// objects have the same class
}else{
// objects have different classes
}
盡可能少用直接名稱比較
不要使用內部類
Java 字節碼沒有內部類的概念因為編譯器將內部類轉換成了普通類而如果沒有將內部類聲明為 private 則同一個包內的任何代碼恰好能訪問該普通類
影響
因為有這一特性所以包內的惡意代碼可以訪問這些內部類如果內部類能夠訪問括起外部類的字段那麼情況會變得更糟可能已經將這些字段聲明為 private 這樣內部類就被轉換成了獨立類但當內部類訪問外部類的字段時編譯器就將這些字段從專用(private)的變為在包(package)的作用域內有效的內部類暴露了已經夠糟糕的了但更糟糕的是編譯器使您將某些字段成為 private 的舉動成為徒勞
建議
如果能夠不使用內部類就不要使用內部類
對付低嚴重性暴露的技巧
請遵循下列建議以避免低嚴重性靜態安全性暴露
·避免返回可變對象
·檢查本機方法
避免返回可變對象
Java 方法返回對象引用的副本如果實際對象是可改變的那麼使用這樣一個引用調用程序可能會改變它的內容通常這是我們所不希望見到的
影響
請考慮這個示例某個方法返回一個對敏感對象的內部數組的引用假定該方法的調用程序不改變這些對象即使數組對象本身是不可改變的也可以在數組對象以外操作數組的 內容這種操作將反映在返回該數組的對象中如果該方法返回可改變的對象那麼事情會變得更糟外部實體可以改變在那個類中聲明的 public 變量這種改變將反映在實際對象中
清單 演示了脆弱性 getExposedObj() 方法返回了 Exposed 對象的 引用副本該對象是可變的
清單 返回可變對象的引用副本
Java代碼
class Exposed{
private int id;
private String name;
public Exposed(){
}
public Exposed(int id String name){
thisid = id;
thisname = name;
}
public int getId(){
return id;
}
public String getName(){
return name;
}
public void setId(int id){
thisid=id;
}
public void setName(String name){
thisname = name;
}
public void display(){
Systemoutprintln(Id = + id + Name = + name);
}
}
public class Exp{
private Exposed exposedObj = new Exposed(Harry Porter);
public Exposed getExposedObj(){
return exposedObj; //returns a reference to the object
}
public static void main(String[] args){
Exp exp = new Exp();
expgetExposedObj()display();
Exposed exposed = expgetExposedObj();
exposedsetId();
exposedsetName(Hacker);
expgetExposedObj()display();
}
}
建議
如果方法返回可改變的對象但又不希望調用程序改變該對象請修改該方法使之不返回實際對象而是返回它的副本或克隆要改正清單 中的代碼請讓它返回 Exposed 對象的 副本如清單 中所示
清單 返回可變對象的副本
Java代碼
public Exposed getExposedObj(){
return new Exposed(exposedObjgetId()exposedObjgetName());
}
或者您的代碼也可以返回 Exposed 對象的克隆
檢查本機方法
本機方法是一種 Java 方法其實現是用另一種編程語言編寫的如 C 或 C++有些開發人員實現本機方法這是因為 Java 語言即使使用即時(justintime)編譯器也比許多編譯過的語言要慢其它人需要使用本機代碼是為了在 JVM 以外實現特定於平台的功能
影響
使用本機代碼時請小心因為對這些代碼進行驗證是不可能的而且本機代碼可能潛在地允許 applet 繞過通常的安全性管理器(Security Manager)和 Java 對設備訪問的控制
建議
如果非得使用本機方法那麼請檢查這些方法以確定
·它們返回什麼
·它們獲取什麼作為參數
·它們是否繞過安全性檢查
·它們是否是 public private 等等
·它們是否含有繞過包邊界從而繞過包保護的方法調用
結束語
編寫安全 Java 代碼是十分困難的但本文描述了一些可行的實踐來幫您編寫安全 Java 代碼這些建議並不能解決您的所有安全性問題但它們將減少暴露數目最佳軟件安全性實踐可以幫助確保軟件正常運行安全至關重要和高可靠系統設計者總是花費大量精力來分析和跟蹤軟件行為只有通過將安全性作為至關緊要的系統特性來對待 ― 並且從一開始就將它構建到應用程序中我們才可以避免亡羊補牢似的修修補補的安全性方法
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26765.html