相關介紹:
我們都知道,程序員編寫一個Java程序,默認的情況下可以訪問該機器的任意資源,比如讀取,刪除一些文件或者網絡操作等。當你把程序部署到正式的服務器上,系統管理員要為服務器的安全承擔責任,那么他可能不敢確定你的程序會不會訪問不該訪問的資源,為了消除潛在的安全隱患,他可能有兩種辦法:
- 讓你的程序在一個限定權限的帳號下運行。
- 利用Java的沙箱機制來限定你的程序不能為非作歹。以下用於介紹該機制。
什么是沙箱?
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一個限制程序運行的環境。沙箱機制就是將 Java 代碼限定在虛擬機(JVM)特定的運行范圍中,並且嚴格限制代碼對本地系統資源訪問,通過這樣的措施來保證對代碼的有效隔離,防止對本地系統造成破壞。沙箱主要限制系統資源訪問,那系統資源包括什么?——CPU、內存、文件系統、網絡。不同級別的沙箱對這些資源訪問的限制也可以不一樣。
所有的Java程序運行都可以指定沙箱,可以定制安全策略。
java中的安全模型:
在Java中將執行程序分成本地代碼和遠程代碼兩種,本地代碼默認視為可信任的,而遠程代碼則被看作是不受信的。對於授信的本地代碼,可以訪問一切本地資源。而對於非授信的遠程代碼在早期的Java實現中,安全依賴於沙箱 (Sandbox) 機制。如下圖所示
但如此嚴格的安全機制也給程序的功能擴展帶來障礙,比如當用戶希望遠程代碼訪問本地系統的文件時候,就無法實現。因此在后續的 Java1.1 版本中,針對安全機制做了改進,增加了安全策略,允許用戶指定代碼對本地資源的訪問權限。如下圖所示
在 Java1.2 版本中,再次改進了安全機制,增加了代碼簽名。不論本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化的代碼執行權限控制。如下圖所示
當前最新的安全機制實現,則引入了域 (Domain) 的概念。虛擬機會把所有代碼加載到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域 (Protected Domain),對應不一樣的權限 (Permission)。存在於不同域中的類文件就具有了當前域的全部權限,如下圖所示
以上提到的都是基本的 Java 安全模型概念,在應用開發中還有一些關於安全的復雜用法,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能夠使一段受信任代碼獲得更大的權限,甚至比調用它的應用程序還要多,可做到臨時訪問更多的資源。有時候這是非常必要的,可以應付一些特殊的應用場景。例如,應用程序可能無法直接訪問某些系統資源,但這樣的應用程序必須得到這些資源才能夠完成功能。
組成沙箱的基本組件:
-
字節碼校驗器(bytecode verifier):確保Java類文件遵循Java語言規范。這樣可以幫助Java程序實現內存保護。但並不是所有的類文件都會經過字節碼校驗,比如核心類。
-
類裝載器(class loader):其中類裝載器在3個方面對Java沙箱起作用
- 它防止惡意代碼去干涉善意的代碼;
- 它守護了被信任的類庫邊界;
- 它將代碼歸入保護域,確定了代碼可以進行哪些操作。
虛擬機為不同的類加載器載入的類提供不同的命名空間,命名空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的,它們互相之間甚至不可見。
類裝載器采用的機制是雙親委派模式。1. 從最內層JVM自帶類加載器開始加載,外層惡意同名類得不到加載從而無法使用;
2. 由於嚴格通過包來區分了訪問域,外層惡意的類通過內置代碼也無法獲得權限訪問到內層類,破壞代碼就自然無法生效。
-
存取控制器(access controller):存取控制器可以控制核心API對操作系統的存取權限,而這個控制的策略設定,可以由用戶指定。
-
安全管理器(security manager):是核心API和操作系統之間的主要接口。實現權限控制,比存取控制器優先級高。
-
安全軟件包(security package):java.security下的類和擴展包下的類,允許用戶為自己的應用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 數字簽名
- 加密
- 鑒別
沙箱包含的要素:
1. 權限
權限是指允許代碼執行的操作。包含三部分:權限類型、權限名和允許的操作。權限類型是實現了權限的Java類名,是必需的。權限名一般就是對哪類資源進行操作的資源定位(比如一個文件名或者通配符、網絡主機等),一般基於權限類型來設置,有的比如java.security.AllPermission不需要權限名。允許的操作也和權限類型對應,指定了對目標可以執行的操作行為,比如讀、寫等。如下面的例子:
permission java.security.AllPermission; //權限類型
permission java.lang.RuntimePermission "stopThread"; //權限類型+權限名
permission java.io.FilePermission "/tmp/foo" "read"; //權限類型+權限名+允許的操作
標准權限:
說明 | 類型 | 權限名 | 操作 | 例子 |
---|---|---|---|---|
文件權限 | java.io.FilePermission | 文件名(平台依賴) | 讀、寫、刪除、執行 | 允許所有問價的讀寫刪除執行:permission java.io.FilePermission "<< ALL FILES>>", "read,write,delete,execute";。允許對用戶主目錄的讀:permission java.io.FilePermission "${user.home}/-", "read"; |
套接字權限 | java.net.SocketPermission | 主機名:端口 | 接收、監聽、連接、解析 | 允許實現所有套接字操作:permission java.net.SocketPermission ":1-", "accept,listen,connect,resolve";。允許建立到特定網站的連接:permission java.net.SocketPermission ".abc.com:1-", "connect,resolve"; |
屬性權限 | java.util.PropertyPermission | 需要訪問的jvm屬性名 | 讀、寫 | 讀標准Java屬性:permission java.util.PropertyPermission "java.", "read";。在sdo包中創建屬性:permission java.util.PropertyPermission "sdo.", "read,write"; |
運行時權限 | java.lang.RuntimePermission | 多種權限名[見附錄A] | 無 | 允許代碼初始化打印任務:permission java.lang.RuntimePermission "queuePrintJob" |
AWT權限 | java.awt.AWTPermission | 6種權限名[見附錄B] | 無 | 允許代碼充分使用robot類:permission java.awt.AWTPermission "createRobot"; permission java.awt.AWTPermission "readDisplayPixels"; |
網絡權限 | java.net.NetPermission | 3種權限名[見附錄C] | 無 | 允許安裝流處理器:permission java.net.NetPermission "specifyStreamHandler";。 |
安全權限 | java.security.SecurityPermission | 多種權限名[見附錄D] | 無 | |
序列化權限 | java.io.SerializablePermission | 2種權限名[見附錄E] | 無 | |
反射權限 | java.lang.reflect.ReflectPermission | suppressAccessChecks(允許利用反射檢查任意類的私有變量) | 無 | |
完全權限 | java.security.AllPermission | 無(擁有執行任何操作的權限) | 無 |
2. 代碼源
代碼源是類所在的位置,表示為以URL地址。
3. 保護域
保護域用來組合代碼源和權限,這是沙箱的基本概念。保護域就在於聲明了比如由代碼A可以做權限B這樣的事情。
4. 策略文件
策略文件是控制沙箱的管理要素,一個策略文件包含一個或多個保護域的項。策略文件完成了代碼權限的指定任務,策略文件包括全局和用戶專屬兩種。
為了管理沙箱,策略文件我認為是最重要的內容。JVM可以使用多個策略文件,不過一般兩個最常用。一個是全局的:$JREHOME/lib/security/java.policy,作用於JVM的所有實例。另一個是用戶自己的,可以存儲到用戶的主目錄下。策略文件可以使用jdk自帶的policytool工具編輯。
默認的策略文件我們先參考一下:
// Standard extensions get all permissions by default
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
// default permissions granted to all domains
grant {
// Allows any thread to stop itself using the java.lang.Thread.stop()
// method that takes no argument.
// Note that this permission is granted by default only to remain
// backwards compatible.
// It is strongly recommended that you either remove this permission
// from this policy file or further restrict it to code sources
// that you specify, because Thread.stop() is potentially unsafe.
// See the API specification of java.lang.Thread.stop() for more
// information.
permission java.lang.RuntimePermission "stopThread";
// allows anyone to listen on dynamic ports
permission java.net.SocketPermission "localhost:0", "listen";
// permission for standard RMI registry port
permission java.net.SocketPermission "localhost:1099", "listen";
// "standard" properies that can be read by anyone
permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";
permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";
permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};
策略文件的內容格式就是這樣,grant授權允許操作某個權限。這個默認的策略文件就指明了jdk擴展包可以有全部權限,允許代碼stop線程,允許監聽1099端口(1099號端口,是默認的服務器端RMI監聽端口)等等。
另一個很重要的是參數文件——java.security,這個文件和策略文件在同一個目錄下。這個參數文件定義了沙箱的一些參數。比如默認的沙箱文件是這樣的(截取部分):
# The default is to have a single system-wide policy file,
# and a policy file in the user's home directory.
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
# whether or not we expand properties in the policy file
# if this is set to false, properties (${...}) will not be expanded in policy
# files.
policy.expandProperties=true
# whether or not we allow an extra policy to be passed on the command line
# with -Djava.security.policy=somefile. Comment out this line to disable
# this feature.
policy.allowSystemProperty=true
policy.url.*這個屬性指明了使用的策略文件,如上文所述,默認的兩個位置就在這里配置,用戶可以自行更改順序和存儲位置。而policy.allowSystemProperty指明是否允許用戶自行通過命令行指定policy文件。
5. 密鑰庫
保存密鑰證書的地方。
默認沙箱
通過Java命令行啟動的Java應用程序,默認不啟用沙箱。要想啟用沙箱,啟動命令需要做如下形式的變更:
java -Djava.security.manager <other args>
沙箱啟動后,安全管理器會使用兩個默認的策略文件來確定沙箱啟動參數。當然也可以通過命令指定:
java -Djava.security.policy=<URL>
如果要求啟動時只遵循一個策略文件,那么啟動參數要加個等號,如下:
java -Djava.security.policy==<URL>
如何使用
1. 限制讀文件
這個例子很簡單,首先寫一個r.txt文件,里面的內容是“abcd”,再寫個程序如下讀取這個r.txt。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class PolicyTest {
public static void file() {
File f = new File("D:\\github\\CDLib\\src\\main\\resources\\security\\r.txt");
InputStream is;
try {
is = new FileInputStream(f);
byte[] content = new byte[1024];
while (is.read(content) != -1) {
System.out.println(new String(content));
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
// test read file.
file();
}
}
發現輸出是abcd。
接下來修改java啟動參數,加入-Djava.security.manager,啟動了安全沙箱。再運行,輸出變成了異常
Exception in thread "main" java.security.AccessControlException: access denied ("java.io.FilePermission" "D:\github\CDLib\src\main\resources\security\r.txt" "read")
at java.security.AccessControlContext.checkPermission(Unknown Source)
at java.security.AccessController.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkPermission(Unknown Source)
at java.lang.SecurityManager.checkRead(Unknown Source)
at java.io.FileInputStream.(Unknown Source)
at com.taobao.cd.security.PolicyTest.main(PolicyTest.java:15)
這里已經提示了,訪問被拒絕,說明了沙箱啟動,同時也驗證了默認沙箱——禁止本地文件訪問。
再來,我們構建一個custom.policy文件如下:
grant {
permission java.io.FilePermission "D:\\github\\CDLib\\src\\main\\resources\\security\\*", "read";
};
這里構建了一條安全策略——允許讀取security目錄下的文件。
修改啟動命令,添加
-Djava.security.policy=D:\\github\\CDLib\\src\\main\\resources\\security\\custom.policy
再執行,結果輸出了abcd。
如上例。我們通過自定義policy文件修改了默認沙箱的安全策略,再通過啟動參數開啟沙箱模式。這樣就可以構造我們自己想要的沙箱效果了。
附錄
A
權限名 | 用途說明 |
---|---|
accessClassInPackage.
|
允許代碼訪問指定包中的類 |
accessDeclaredMembers | 允許代碼使用反射訪問其他類中私有或保護的成員 |
createClassLoader | 允許代碼實例化類加載器 |
createSecurityManager | 允許代碼實例化安全管理器,它將允許程序化的實現對沙箱的控制 |
defineClassInPackage.
|
允許代碼在指定包中定義類 |
exitVM | 允許代碼關閉整個虛擬機 |
getClassLoader | 允許代碼訪問類加載器以獲得某個特定的類 |
getProtectionDomain | 允許代碼訪問保護域對象以獲得某個特定類 |
loadlibrary.
|
允許代碼裝載指定類庫 |
modifyThread | 允許代碼調整指定的線程參數 |
modifyThreadGroup | 允許代碼調整指定的線程組參數 |
queuePrintJob | 允許代碼初始化一個打印任務 |
readFileDescriptor | 允許代碼讀文件描述符(相應的文件是由其他保護域中的代碼打開的) |
setContextClassLoader | 允許代碼為某線程設置上下文類加載器 |
setFactory | 允許代碼創建套接字工廠 |
setIO | 允許代碼重定向System.in、System.out或System.err輸入輸出流 |
setSecurityManager | 允許代碼設置安全管理器 |
stopThread | 允許代碼調用線程類的stop()方法 |
writeFileDescriptor | 允許代碼寫文件描述符 |
B
權限名 | 用途說明 |
---|---|
accessClipboard | 允許訪問系統的全局剪貼板 |
accessEventQueue | 允許直接訪問事件隊列 |
createRobot | 允許代碼創建AWT的Robot類 |
listenToAllAWTEvents | 允許代碼直接監聽事件分發 |
readDisplayPixels | 允許AWT Robot讀顯示屏上的像素 |
showWindowWithoutWarningBanner | 允許創建無標題欄的窗口 |
C
權限名 | 用途說明 |
---|---|
specifyStreamHandler | 允許在URL類中安裝新的流處理器 |
setDefaultAuthenticator | 可以安裝鑒別類 |
requestPassworkAuthentication | 可以完成鑒別 |
D
權限名 | 用途說明 |
---|---|
addIdentityCertificate | 為Identity增加一個證書 |
clearProviderProperties.
|
針對指定的提供者,刪除所有屬性 |
createAccessControlContext | 允許創建一個存取控制器的上下文環境 |
getDomainCombiner | 允許撤銷保護域 |
getPolicy | 檢索可以實現沙箱策略的類 |
getProperty.
|
讀取指定的安全屬性 |
getSignerPrivateKey | 由Signer對象獲取私有密鑰 |
insertProvider.
|
將指定的提供者添加到響應的安全提供者組中 |
loadProviderProperties.
|
裝載指定的提供者的屬性 |
printIdentity | 打印Identity類內容 |
putAllProviderProperties.
|
更新指定的提供者的屬性 |
putProviderProperty.
|
為指定的提供者增加一個屬性 |
removeIdentityCertificate | 取消Identity對象的證書 |
removeProvider.
|
將指定的提供者從相應的安全提供者組中刪除 |
removeProviderProperty.
|
刪除指定的安全提供者的某個屬性 |
setIdentityInfo | 為某個Identity對象設置信息串 |
setIdentityPublicKey | 為某個Identity對象設置公鑰 |
setPolicy | 設置可以實現沙箱策略的類 |
setProperty.
|
設置指定的安全屬性 |
setSignerKeyPair | 在Signer對象中設置密鑰對 |
setSystemScope | 設置系統所用的IdentityScope |
E
權限名 | 用途說明 |
---|---|
enableSubstitution | 允許實現ObjectInputStream類的enableResolveObject()方法和ObjectOutputStream類的enableReplaceObject()方法 |
enableSubclassImplementation | 允許ObjectInputStream和ObjectOutputStream創建子類,子類可以覆蓋readObject()和writeObject()方法 |