本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.2节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。
指南2:不要在客户端存储未经加密的敏感数据
在使用客户端服务器模式构建应用程序时,如果客户端容易受到攻击,那么将用户凭证信息这样的敏感数据存储在客户端,会导致这些信息在未经授权的情况下被泄露。
对于Web应用程序,最常见的缓解这个问题的方法,就是为客户端提供一个cookie,将敏感信息存储在服务器上。这些cookie由Web服务器创建,只存储在客户端上,一段时间以后就会失效。当客户端重新连接至服务器时,必须提供相应的cookie,服务器用它来识别客户端,然后为识别后的客户端提供敏感信息。
cookie并不能防止敏感信息免受跨站脚本(Cross-Site Scripting,XSS)攻击。只要攻击者通过XSS攻击获得cookie,或者通过攻击客户端直接获得cookie,就可以使用这些cookie从服务器上获得敏感信息。如果一个会话超出限制时间(如15分钟)后,服务器端随即让该会话失效,那么这样的风险是可以被时间控制的。
一个cookie通常是一个短字符串。如果它包含敏感信息,那么这些信息应该被加密。敏感信息包括用户名、密码、信用卡号码、社会保险号以及任何可识别该用户的个人信息。更多关于密码管理的细节,参见指南13。更多有关保护存有敏感信息的内存的信息,参见指南1。
违规代码示例
在下面的违规代码示例中,登录程序将用户名和密码存储至cookie中,用以识别该用户并允许后续请求。
protected void doPost(HttpServletRequest request,
HttpServletResponse response) {
// Validate input (omitted)
String username = request.getParameter("username");
char[] password =
request.getParameter("password").toCharArray();
boolean rememberMe =
Boolean.valueOf(request.getParameter("rememberme"));
LoginService loginService = new LoginServiceImpl();
if (rememberMe) {
if (request.getCookies()[0] != null &&
request.getCookies()[0].getValue() != null) {
String[] value =
request.getCookies()[0].getValue().split(";");
if (!loginService.isUserValid(value[0],
value[1].toCharArray())) {
// Set error and return
} else {
// Forward to welcome page
}
} else {
boolean validated =
loginService.isUserValid(username, password);
if (validated) {
Cookie loginCookie = new Cookie("rememberme", username +
";" + new String(password));
response.addCookie(loginCookie);
// ... forward to welcome page
} else {
// Set error and return
}
}
} else {
// No remember-me functionality selected
// Proceed with regular authentication;
// if it fails set error and return
}
Arrays.fill(password, ' ');
}```
不管怎样,用以上述方式实现“记住我”的功能,是不安全的,因为攻击者只要能访问客户端机器就能直接在客户端上获得这些敏感信息。这段代码也违背了指南13。
合规解决方案(会话)
在下面的合规解决方案中,程序是通过在cookie中只存储一个用户名和一个安全随机字符串来实现“记住我”功能的。同时程序还使用了HttpSession来管理会话状态。
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
// Validate input (omitted)
String username = request.getParameter("username");
char[] password =
request.getParameter("password").toCharArray();
boolean rememberMe =
Boolean.valueOf(request.getParameter("rememberme"));
LoginService loginService = new LoginServiceImpl();
boolean validated = false;
if (rememberMe) {
if (request.getCookies()[0] != null &&
request.getCookies()[0].getValue() != null) {
String[] value =
request.getCookies()[0].getValue().split(";");
if (value.length != 2) {
// Set error and return
}
if (!loginService.mappingExists(value[0], value[1])) {
// (username, random) pair is checked
// Set error and return
}
} else {
validated = loginService.isUserValid(username, password);
if (!validated) {
// Set error and return
}
}
String newRandom = loginService.getRandomString();
// Reset the random every time
loginService.mapUserForRememberMe(username, newRandom);
HttpSession session = request.getSession();
session.invalidate();
session = request.getSession(true);
// Set session timeout to 15 minutes
session.setMaxInactiveInterval(60 * 15);
// Store user attribute and a random attribute
// in session scope
session.setAttribute("user", loginService.getUsername());
Cookie loginCookie =
new Cookie("rememberme", username + ";" + newRandom);
response.addCookie(loginCookie);
// ... forward to welcome page
} else { // No remember-me functionality selected
// ... authenticate using isUserValid(),
// and if failed, set error
}
Arrays.fill(password, ' ');
}`
服务器维护了一个用户名与安全随机字符串之间的映射。当用户选择“记住我”时,doPost()方法会检查请求提供的cookie是否包含一个有效的用户名和随机字符串对。如果在映射中找到了这个用户名和随机字符串对,那么服务器会对这个用户进行身份验证,并在验证成功的情况下跳转到该用户的欢迎页面。如果没有找到,服务器会向客户端返回一个错误。如果用户选择了“记住我”,但客户端未能提供一个有效的cookie,那么服务器会要求用户使用他的凭据进行身份验证。如果身份验证成功,服务器会给客户端发送一个新的具有“记住我”特征的cookie。
上述解决方案,通过在验证完毕后立即令当前会话失效并重新创建一个新的会话,避免了“会话固化”攻击;另外,通过将客户端访问会话超时时间设置为15分钟,也减小了攻击者进行“会话劫持”攻击的机会。
适用性
在客户端存储未经加密的敏感信息,导致信息泄露给客户端的攻击者。