【问题标题】:How do I securely handle passwords in a Java Servlet Filter?如何安全地处理 Java Servlet 过滤器中的密码?
【发布时间】:2014-03-11 03:40:00
【问题描述】:

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

我不关心如何处理身份验证、401 加上 WWW-Authenticate 响应标头、JDBC 查找或类似的东西。我的过滤器效果很好。

我担心的是我们永远不应该将用户密码存储在 java.lang.String 中,因为它们是不可变的。完成身份验证后,我无法将那个字符串归零。该对象将位于内存中,直到垃圾收集器运行。这为坏人打开了一个更大的窗口来获取核心转储或以其他方式观察堆。

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

如果我切换到某种形式的基于表单的安全性,问题仍然存在。 javax.servlet.ServletRequest.getParameter(String) 也返回一个字符串。

这仅仅是 Java EE 的限制吗?

【问题讨论】:

    标签: java security servlets filter passwords


    【解决方案1】:

    实际上只有字符串字面量保存在 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(即避免使用字符串)来执行身份验证。

    /**
     * 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);
    
    }
    

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

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

    stackoverflow 详解中有一个惊人的答案

    Use servlet filter to remove a form parameter from posted data

    还有一个重要的解释:

    方法

    代码遵循正确的方法:

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

    public String getParameter(字符串名称) 公共地图 getParameterMap() 公共枚举 getParameterNames() 公共字符串[] getParameterValues(字符串名称) doFilter() 方法使用包装的请求调用过滤器链,这意味着后续过滤器以及目标 servlet(URL 映射)将被提供包装的请求。

    【讨论】:

    • 我承认基本身份验证的局限性。但是,当使用 HTTPS 时,密码不会以明文形式通过网络发送。 HTTPS 可以防止窃听,因此希望用户浏览器和我的服务器之间的连接是保密的。我的服务器是运行 Servlet 过滤器的服务器。
    • 是的。你说的对。密码不会以明文形式通过网络发送。但是,在 Tomcat 应用服务器中,默认情况下,每个标准 Realm 实现的用户密码都以明文形式存储。有时,您还可以在带有 ajp 连接器的 Tomcat 服务器前面使用 Apache 服务器。最后,如果你直接使用Tomcat,HTTPS会阻止窃听。
    • 好的,我明白你为什么指出这一点了。我们的应用程序中已经有一个包含哈希密码字段的用户表。我的 Servlet 过滤器对其进行身份验证。
    • 我认为我不需要担心包装请求。我可以使用request.getCoyoteRequest.getMimeHeaders().getValue(String) 方法获取密码的char[],然后在我执行身份验证后甚至可以将其归零。我并不热衷于将它绑定到 Tomcat,因为我们有一些运行不同 Web 容器的客户。如果不是 Tomcat,我可以使用默认的 getHeader(String) 方法,而当我在 Tomcat 中时这种疯狂的方式。哇。
    • 谢谢@rdllopes。最终,我们不会将供应商特定的东西放在我们的 servlet 中,即使它是 Tomcat。我确实认为我的问题得到了回答,因为您已经确认 J2EE 没有为此提供机制并且因为您提供了一种解决方法。谁知道,我们可能有一天会回到它。
    【解决方案2】:

    这是正确的,但它永远不应该在 db 中存储要检查的实际密码,而是在密码本身上存储一个哈希值,然后运行一个哈希值来确定这两个哈希值是否相同,即原始用户从未使用过密码。

    【讨论】:

    • 我不关心 JDBC 位(原始问题的第二段)。我正在使用 bcrypt 哈希。
    【解决方案3】:

    如果您对此感到担忧,请在您的Filter 中使用ServletRequest.getInputStream() 而不是HttpServletRequest.getHeader(String)。您应该能够以流的形式获取 HTTP 请求,跳过直到到达 Authorization 标头并在 char [] 中获取密码。

    但是所有这些努力可能都是徒劳的,因为底层对象仍然是 HTTPServletRequest 并且可能包含所有标头作为映射中的键 val 对,详细信息取决于 servlet 的实现方式。

    【讨论】:

    • 对不起,getInputStream 只返回请求的正文。标题在正文之前。不过你是对的,容器可能会使这个无效(因此我对原始问题中从 Socket 到 HttpServletRequest 的请求的评论)。
    • 我的错误只是将我的思考过程放入这个答案中而没有测试它
    猜你喜欢
    • 2015-11-21
    • 1970-01-01
    • 2020-01-03
    • 2015-07-07
    • 1970-01-01
    • 2011-09-13
    • 2016-02-12
    • 1970-01-01
    相关资源
    最近更新 更多