《Java编码指南:编写安全可靠程序的75条建议》—— 指南1:限制敏感数据的生命周期

本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.1节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。

指南1:限制敏感数据的生命周期

内存中的敏感信息很容易受到攻击,导致泄漏。对于以下条件,不管应用程序满足哪一个,能在应用程序所在的系统上执行代码的攻击者,都能访问这些数据。

使用对象来存储敏感数据,但是内容在使用后没有被清除或没有被垃圾收集器回收。
具有可以被操作系统按需(例如,为了执行内存管理任务或者为了支持系统休眠)交换到磁盘的内存页面。
在缓冲区(如BufferedReader)中有敏感数据。这些缓冲区持有敏感数据在操作系统缓存或内存中的副本。
使用了一些反射机制来控制敏感数据的流动,这些反射技巧可能规避掉系统对该数据的生命周期限制。
通过调试信息、日志文件、环境变量、线程转储和核心转储等方式暴露敏感数据。
如果内存中包含的敏感数据在使用后没有及时被清除,那么这些数据将极有可能被泄露。为了降低敏感数据泄露的风险,程序必须尽可能地最小化敏感数据的生命周期。

完全缓解(即对内存数据万无一失的保护)需要底层操作系统和Java虚拟机的支持。例如,如果将敏感数据交换至磁盘是一个问题,那么就需要有一个禁用交换和休眠的安全操作系统。

违规代码示例
在以下违规代码示例中,程序从控制台读取用户名和密码信息,并将密码存储在一个String对象中。在垃圾收集器回收这个String对象关联的内存之前,凭证信息会一直处于暴露状态。

class Password {
 public static void main (String args[]) throws IOException {
  Console c = System.console();
  if (c == null) {
   System.err.println("No console.");
   System.exit(1);
  }

  String username = c.readLine("Enter your user name: ");
  String password = c.readLine("Enter your password: ");

  if (!verify(username, password)) {
   throw new SecurityException("Invalid Credentials");
  }

  // ...
 }

 // Dummy verify method, always returns true
 private static final boolean verify(String username, 
   String password) {
  return true;
 }
}```
合规解决方案
下面的合规解决方案使用Console.readPassword()方法从控制台获取密码信息。

class Password {
 public static void main (String args[]) throws IOException {
  Console c = System.console();

  if (c == null) {
   System.err.println("No console.");
   System.exit(1);
  }

  String username = c.readLine("Enter your user name: ");
  char[] password = c.readPassword("Enter your password: ");

  if (!verify(username, password)) {
   throw new SecurityException("Invalid Credentials");
  }

  // Clear the password
  Arrays.fill(password, ' ');
 }

 // Dummy verify method, always returns true
 private static final boolean verify(String username,
   char[] password) {
  return true;
 }
}`
Console.readPassword()方法允许密码以字符序列的形式返回,而不是以String对象的形式。因此,程序员可以在使用密码后立即将其从数组中清除。同时这个方法也禁止将密码输出到控制台。

违规代码示例
下面的违规代码示例使用了一个BufferedReader来包装InputStreamReader对象,导致敏感数据可以从文件中被读取。

void readData() throws IOException{
 BufferedReader br = new BufferedReader(new InputStreamReader(
  new FileInputStream("file")));
 // Read from the file
 String data = br.readLine();
}```
BufferedReader.readLine()方法以String对象的形式返回敏感数据,导致数据在不被需要后仍然会存留很久。BufferedReader.read(char[], int, int)方法可以读取和填充一个char数组,不过,它需要程序员在使用完数组中的敏感数据后,手动将其清除,否则就会导致泄漏。另外,尽管BufferedReader是用来包装FileReader对象的,但它同样也会遭遇相似的陷阱。

合规解决方案
在下面的合规解决方案中,程序使用了一个直接分配的新I/O(new I/O,NIO)缓冲区从文件中读取敏感数据。这些数据可以在使用后被立即清除,并且不会被缓存或缓冲在多个位置上,它只会出现在系统内存中。

void readData(){
 ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
 try (FileChannel rdr =
    (new FileInputStream("file")).getChannel()) {
  while (rdr.read(buffer) > 0) {
   // Do something with the buffer
   buffer.clear();
  }
 } catch (Throwable e) {
  // Handle error
 }
}`
注意,必须手动清除缓冲数据,因为垃圾收集器不会回收直接缓冲区。

适用性
对敏感数据生命周期限制失败,导致信息泄露。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

上一篇:应该知道的c知识点


下一篇:【java web】Servlet生命周期