【Java編碼準則】の #02不要在客戶端儲存未加密的敏感資訊
當構建CS模式的應用程式時,在客戶端側儲存敏感資訊(例如使用者私要資訊)可能導致非授權的資訊洩漏。
對於Web應用程式來說,最常見的洩漏問題是在客戶端使用cookies存放伺服器端獲取的敏感資訊。Cookies是由web伺服器建立的,它具有一個指定的有效時間,儲存在客戶端。當客戶端連線上伺服器端時,客戶端使用cookies中儲存的資訊向伺服器端進行認證,通過後伺服器端返回敏感資訊。
在XSS攻擊下,Cookies不能保證敏感資訊的安全。無論是通過XSS攻擊,還是直接對客戶端的攻擊,攻擊者一旦獲取到Cookies,他就可以使用這個Cookies從伺服器端獲取敏感資訊。上面的風險存在時間窗,當Cookies存活超過指定的時間後(例如15分鐘),伺服器端會使會話無效,這時風險就不存在了。
Cookies是一個短的字串,如果它包含了敏感的資訊,那麼這段資訊必須進行加密,敏感資訊包括使用者名稱,密碼,信用卡號碼,社會安全碼,以及其他任何個人標識資訊。關於管理密碼的更多細節,參見“#13使用雜湊函式來儲存密碼”。關於如何保證記憶體中敏感資訊保安的更多細節,參見“#01限制記憶體中敏感資料的生命週期”。
[不符合安全要求的程式碼示例]
下面的程式碼中,login servlet將使用者名稱和密碼儲存在Cookies中,用於後續請求中標識使用者。
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); // forword to welcome page } else { // set error and return } } } else { // no remember-me functionality selected // process with regular authentication; // if it fails set error and return } Array.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 = reuqest.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("uset", loginService.getUsername()); Cookie loginCookie = new Cookie("rememberme", username + ";" + newRandom); response.addCookie(loginCookie); // forword to welcome page } } else { // no remember-me functionality selected // process with regular authentication; // if it fails set error and return } Array.fill(password, ' '); }
伺服器端儲存使用者名稱和安全隨機字串的對映關係,當用戶選擇“記住我”時,doPost()函式檢查客戶端提供的Cookies中是否包含有效的使用者名稱和隨機字串對映對。如果對映對是正確的,伺服器端通過該使用者的認證,並使使用者跳轉到歡迎頁。如果認證沒有通過,伺服器端返回錯誤給客戶端。如果使用者選擇“記住我”,但客戶度沒有有效的Cookie導致認證失敗,那麼伺服器端會要求使用者使用認證資訊重新進行認證。如果認證成功,伺服器端會提供一個包含新的“記住我”特性的Cookie給客戶端。
這個解決方案通過使當前會話無效並建立新的會話,可以避免固定會話攻擊。同時通過將會話訪問有效時間設定為15分鐘來將減少攻擊者實施會話劫持攻擊的時間窗長度。