冷門的 Java 應用程式安全沙箱機制瞭解一下

老錢發表於2019-03-02

如果你經常閱讀原始碼,你會發現 Java 的原始碼中到處都有類似於下面這一段程式碼

class File {
  // 判斷一個磁碟檔案是否存在
  public boolean exists() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
      security.checkRead(path);
    }
    ...
  }
}
複製程式碼

這明顯是一個安全檢查程式碼,檢查的是你是否有訪問磁碟路徑的許可權,為什麼 Java 語言需要這樣的安全檢查程式碼呢?我們再看看客戶端套接字的 connect 函式原始碼,它需要檢查使用者是否有connect 某個網路地址的許可權

class Socket {
  public void connect(SocketAddress endpoint, int timeout) {
    ...
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
       if (epoint.isUnresolved())
          security.checkConnect(epoint.getHostName(), port);
       else
          security.checkConnect(addr.getHostAddress(), port);
       }
    }
    ...
  }
}
複製程式碼

再看服務端套接字的原始碼,它會檢查埠的監聽許可權

class ServerSocket {
  public void bind(SocketAddress endpoint, int backlog) {
    ...
    SecurityManager security = System.getSecurityManager();
    if (security != null)
       security.checkListen(epoint.getPort());
    ...
  }
}
複製程式碼

似乎所有和 IO 操作有關的方法呼叫都需要進行安全檢查。跟 IO 操作相關的許可權檢查似乎還可以理解,不是所有的 IO 資源使用者程式都是可以隨意訪問的。但是連環境變數都不讓隨意讀,而且限制的還不是所有環境變數,而是某個具體的環境變數,這安全檢查是不是有點過了?

class System {
  public static String getenv(String name) {
    SecurityManager sm = getSecurityManager();
    if (sm != null) {
       sm.checkPermission(new RuntimePermission("getenv."+name));
    }
    return ProcessEnvironment.getenv(name);
  }
}
複製程式碼

這是因為 Java 的安全檢查管理器和作業系統的許可權檢查不是一個概念,Java 編寫的不只是服務端應用程式,它還可以作為客戶端跑在瀏覽器上(Applet),它還可以以 app 的形式跑在手機上(J2ME),針對不同的平臺 JVM 會使用不同的安全策略。對於 Applet 而言,受限尤其嚴苛,通常都不允許 Applet 來操作本地檔案。待 Java 的安全檢查通過後執行具體的 IO 操作時,作業系統還會繼續進行許可權檢查。

我們平時在本地執行 java 程式時通常都不會預設開啟安全檢查器,需要執行 jvm 引數才會開啟

$ java -Djava.security.manager xxx
$ java -Djava.security.manager -DDjava.security.policy="${policypath}"
複製程式碼

因為安全限制條件可以定製,所以還需要提供具體的安全策略檔案路徑,預設的策略檔案路徑是 JAVA_HOME/jre/lib/security/java.policy,下面讓我們來看看這個檔案裡都寫了些什麼

// 內建擴充套件庫授權規則
// 表示 JAVA_HOME/jre/lib/ext/ 目錄下的類庫可以全權訪問任意資源
// 包含 javax.swing.*, javax.xml.*, javax.crypto.* 等等
grant codeBase "file:${{java.ext.dirs}}/*" {
 permission java.security.AllPermission;
};

// 其它類庫授權規則
grant {
 // 允許執行緒呼叫自己的 stop 方法自殺
 permission java.lang.RuntimePermission "stopThread";
 // 允許程式監聽 localhost 的隨機可用埠,不允許隨意訂製埠
 permission java.net.SocketPermission "localhost:0", "listen";
 // 限制獲取系統屬性,下面一系列的配置都是隻允許讀部分內建屬性
 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 如果提供了 codeBase 引數就是針對具體的類庫來配置許可權規則,如果沒有指定 codeBase 就是針對所有其它類庫配置的規則。

安全檢查沒有通過,那就會丟擲 java.security.AccessControlException 異常。即使安全檢查通過了,作業系統的許可權檢查仍然可能通不過,這時候又會丟擲其它型別的異常。

授權規則採用白名單,依據上面的配置意味著啟用預設安全策略的 JVM 將無法訪問本地檔案。如果需要訪問本地檔案,可以增加下面的規則

permission java.io.FilePermission "/etc/passwd", "read";
permission java.io.FilePermission "/etc/shadow", "read,write";
permission java.io.FilePermission "/xyz", "read,write,delete";
// 允許讀所有檔案
permission java.io.FilePermission "*", "read";
複製程式碼

Permission 的配置引數正好對應了它的構造器引數

public FilePermission(String path, String actions) {
  super(path);
  init(getMask(actions));
}
複製程式碼

Java 預設安全規則分為幾大模組,每個模組都有各自的配置引數

圖片

其中 AllPermission 表示開啟所有許可權。還有一個不速之客 HibernatePermission,它並不是內建的許可權模組,它是 Hibernate 框架為自己訂製的,這意味著安全規則是支援自定義擴充套件的。擴充套件也很簡單,可以自己編寫一個 Permission 子類,實現它的 4 個抽象方法。

abstract class Permission {
  // 許可權名稱,對於檔案來說就是檔名,對於套接字來說就是套接字地址
  // 它的意義是子類可定製的
  private String name;
  // 當前許可權物件是否隱含了 other 許可權
  // 比如 AllPermission 的這個方法總是返回 true
  public abstract boolean implies(Permission other);
  // equals 和 hashcode 用於許可權比較
  public abstract boolean equals(Object obj);
  public abstract int hashCode();
  // 許可權選項 read,write,xxx
  public abstract String getActions();
}

class CustomPermission extends Permission {
  private String actions;
  CustomPermission(string name, string actions) {
    super(name)
    this.actions = actions;
  }
  ...
}
複製程式碼

JVM 啟動時會將 profile 裡面定義的許可權規則載入到許可權池中,使用者程式在特定的 API 方法裡使用許可權池來判斷是否包含呼叫這個 API 的許可權,最終會落實到呼叫許可權池中每一個許可權物件的 implies 方法來判斷是否具備指定許可權。

class CustomAPI {
  public void someMethod() {
    SecurityManager sec = System.getSecurityManager();
    if(sec != null) {
      sec.CheckPermission(new CustomPermission("xname", "xactions"));
    }
    ...
  }
}
複製程式碼

啟用安全檢查,將會降低程式的執行效率,如果 profile 裡面定義的許可權規則特別多,那麼檢查效率就會很慢,使用時注意安全檢查要省著點使用。

沙箱的安全檢查點非常多,下面列舉一些常見的場景

  1. 檔案操作
  2. 套接字操作
  3. 執行緒和執行緒組
  4. 類載入器控制
  5. 反射控制
  6. 執行緒堆疊資訊獲取
  7. 網路代理控制
  8. Cookie 讀寫控制

如果你的服務端程式開啟了安全檢查,就需要在 policy 配置檔案裡開啟很多安全設定,非常繁瑣,而且配置多了,檢查的效能也會產生一定損耗。這點有點類似 Android 的應用許可權設定,在每個 Android 應用的配置檔案裡都需要羅列出一系列應用子許可權。不過用 Java 來編寫服務端程式似乎開啟安全檢查沒有任何必要。

相關文章