如何在Java Servlet过滤器中安全地处理密码?

bma*_*ter 5 java security passwords servlets filter

我有一个过滤器,通过HTTPS处理BASIC身份验证.这意味着有一个名为"Authorization"的标题,其值类似于"Basic aGVsbG86c3RhY2tvdmVyZmxvdw ==".

我不关心如何处理身份验证,401加WWW-Authenticate响应头,JDBC查找或类似的东西.我的过滤器工作得很漂亮.

我担心的是我们永远不应该在java.lang.String中存储用户密码,因为它们是不可变的.一旦我完成身份验证,我就无法将String归零.该对象将驻留在内存中,直到垃圾收集器运行.这为一个坏人提供了一个更宽的窗口,可以获得核心转储,或以其他方式观察堆.

问题是我看到的唯一方法是通过该javax.servlet.http.HttpServletRequest.getHeader(String)方法读取Authorization标头,但它返回一个String.我需要一个getHeader方法,它返回一个字节或字符数组.理想情况下,请求永远不应该是任何时间点的字符串,从Socket到HttpServletRequest以及它们之间的任何地方.

如果我切换到一些基于表单的安全性,问题仍然存在. javax.servlet.ServletRequest.getParameter(String)也返回一个String.

这仅仅是Java EE的限制吗?

rdl*_*pes 4

实际上,Permgen 的字符串池区域中仅保留字符串文字。创建的字符串是一次性的。

所以...内存转储可能是基本身份验证的小问题之一。其他还有:

  • 密码以明文形式通过线路发送。
  • 对于每个请求,都会重复发送密码。(更大的攻击窗口)
  • 密码由网络浏览器缓存,至少在窗口/进程的长度内。(可以由任何其他对服务器的请求静默重用,例如 CSRF)。
  • 如果用户请求,密码可以永久存储在浏览器中。(与上一点相同,此外可能被共享计算机上的其他用户窃取)。
  • 即使使用 SSL,内部服务器(SSL 协议后面)也可以访问纯文本可缓存密码。

与此同时,Java容器已经解析了HTTP请求并填充了对象。因此,这就是为什么您从请求标头获取 String 的原因。您可能应该重写 Web 容器来解析安全 HTTP 请求。

更新

我错了。至少对于 Apache Tomcat 来说是这样。

http://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/catalina/authenticator/BasicAuthenticator.java.shtml

您可以看到,Tomcat 项目中的 BasicAuthenticator 使用 MessageBytes(即避免使用 String)来执行身份验证。

/**
 * Authenticate the user making this request, based on the specified
 * login configuration.  Return <code>true if any specified
 * constraint has been satisfied, or <code>false if we have
 * created a response challenge already.
 *
 * @param request Request we are processing
 * @param response Response we are creating
 * @param config    Login configuration describing how authentication
 *              should be performed
 *
 * @exception IOException if an input/output error occurs
 */
public boolean authenticate(Request request,
                            Response response,
                            LoginConfig config)
    throws IOException {

    // Have we already authenticated someone?
    Principal principal = request.getUserPrincipal();
    String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE);
    if (principal != null) {
        if (log.isDebugEnabled())
            log.debug("Already authenticated '" + principal.getName() + "'");
        // Associate the session with any existing SSO session
        if (ssoId != null)
            associate(ssoId, request.getSessionInternal(true));
        return (true);
    }

    // Is there an SSO session against which we can try to reauthenticate?
    if (ssoId != null) {
        if (log.isDebugEnabled())
            log.debug("SSO Id " + ssoId + " set; attempting " +
                      "reauthentication");
        /* Try to reauthenticate using data cached by SSO.  If this fails,
           either the original SSO logon was of DIGEST or SSL (which
           we can't reauthenticate ourselves because there is no
           cached username and password), or the realm denied
           the user's reauthentication for some reason.
           In either case we have to prompt the user for a logon */
        if (reauthenticateFromSSO(ssoId, request))
            return true;
    }

    // Validate any credentials already included with this request
    String username = null;
    String password = null;

    MessageBytes authorization = 
        request.getCoyoteRequest().getMimeHeaders()
        .getValue("authorization");

    if (authorization != null) {
        authorization.toBytes();
        ByteChunk authorizationBC = authorization.getByteChunk();
        if (authorizationBC.startsWithIgnoreCase("basic ", 0)) {
            authorizationBC.setOffset(authorizationBC.getOffset() + 6);
            // FIXME: Add trimming
            // authorizationBC.trim();

            CharChunk authorizationCC = authorization.getCharChunk();
            Base64.decode(authorizationBC, authorizationCC);

            // Get username and password
            int colon = authorizationCC.indexOf(':');
            if (colon < 0) {
                username = authorizationCC.toString();
            } else {
                char[] buf = authorizationCC.getBuffer();
                username = new String(buf, 0, colon);
                password = new String(buf, colon + 1, 
                        authorizationCC.getEnd() - colon - 1);
            }

            authorizationBC.setOffset(authorizationBC.getOffset() - 6);
        }

        principal = context.getRealm().authenticate(username, password);
        if (principal != null) {
            register(request, response, principal, Constants.BASIC_METHOD,
                     username, password);
            return (true);
        }
    }


    // Send an "unauthorized" response and an appropriate challenge
    MessageBytes authenticate = 
        response.getCoyoteResponse().getMimeHeaders()
        .addValue(AUTHENTICATE_BYTES, 0, AUTHENTICATE_BYTES.length);
    CharChunk authenticateCC = authenticate.getCharChunk();
    authenticateCC.append("Basic realm=\"");
    if (config.getRealmName() == null) {
        authenticateCC.append(request.getServerName());
        authenticateCC.append(':');
        authenticateCC.append(Integer.toString(request.getServerPort()));
    } else {
        authenticateCC.append(config.getRealmName());
    }
    authenticateCC.append('\"');        
    authenticate.toChars();
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    //response.flushBuffer();
    return (false);

}
Run Code Online (Sandbox Code Playgroud)

只要您可以访问 org.apache.catalina.connector.Request,就不用担心。

那么,如何避免 HTTP 请求的解析

stackoverflow 详细信息中有一个令人惊奇的答案

使用 servlet 过滤器从发布的数据中删除表单参数

以及一个重要的解释:

方法

代码遵循正确的方法:

在wrapRequest()中,它实例化了HttpServletRequestWrapper并重写了触发请求解析的4个方法:

public String getParameter(String name) public Map getParameterMap() public Enumeration getParameterNames() public String[] getParameterValues(String name) doFilter() 方法使用包装的请求调用过滤器链,意味着后续过滤器,加上目标 servlet (URL -mapped) 将提供打包的请求。