最近本司在推行Java 安全编程(当然过一阵也会有C、C++甚至别的语言)的半强制性培训。培训的内容和练习都非常棒,但是因为某些原因仍然有一些同学没用通过考核(70%+ points)。这给了一个可以在较长一段时间内灌水的话题了
。今天第一讲。
控制敏感信息的生命周期
内存中的敏感信息可能会成为被攻击的目标。一个可以在同一个服务器上运行代码的潜在敌手(永远不要相信任何人,Admin 并不意味着他可以/需要访问应用的所有数据,也不意味着他就必须[永远]是一个好人)可能可以访问这些数据,如果,
- 存留敏感信息的对象没用清除内容或者使用后没有被回收
- 内存页被OS换出到硬盘(比如因为休眠)
- 在缓冲区(比如BufferedReader)中存储敏感信息,而OS可能在缓存或者内存中只有该缓冲区的多个拷贝。
- 使用反射不当造成存有敏感信息的对象的生命周期不可控
- 敏感信息存留在调试信息,日志文件,环境变量里,或者被thread dump或者core dump导出。
如果使用之后不被清楚,敏感信息很可能会泄露。为了降低这种分险,程序员需要减少敏感信息的生命周期。当然,完全的防御需要操作系统和JVM的支持,比如被标记为包含敏感信息的内存页不应被交换到磁盘。
不合规代码示例1.1
这段代码从控制台读取用户名和密码并把密码存储在String对象里。密码信息将一直处于易暴露状态直到相关内存被GC回收。
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,
private static final boolean verify(String username, String password) {
return true;
}
}
合规代码示例1.1
合规的代码须用 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
private static final boolean verify(String username, char[] password) {
return true;
}
}
Console.readPassword()
方法返回 char[] 而不是String。程序员可以在使用密码之后马上清空内容,另外,使用此方法还可以密码输入时在控制台回显。
不合规代码示例1.2
以下代码用 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
对象, 它会一直留存在String 常量池里。而 BufferedReader.read(char[], int, int)
方法会把结果读到 char
数组里。当然,这仍需要程序员在使用完敏感信息之后手工清除char数组。同样地,用BufferedReader
包装 FileReader
对象时也会遇到相同的问题。
合规代码示例1.2
可以用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
}
}
注意,手工清除缓冲区是必须的 ,因为直接缓冲区不会被GC回收.