简体   繁体   English

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

[英]How do I securely handle passwords in a Java Servlet Filter?

I have a filter that handles BASIC authentication over HTTPS. 我有一个过滤器,用于处理基于HTTPS的BASIC身份验证。 That means there's a header coming in named "Authorization" and the value is something like "Basic aGVsbG86c3RhY2tvdmVyZmxvdw==". 这意味着有一个名为“ Authorization”的标头,其值类似于“ Basic aGVsbG86c3RhY2tvdmVyZmxvdw ==“。

I'm not concerned with how to handle the authentication, the 401 plus WWW-Authenticate response header, JDBC lookup or anything like that. 我不关心如何处理身份验证,401加上WWW-Authenticate响应标头,JDBC查找或类似的东西。 My filter works beautifully. 我的过滤器效果很好。

My concern is that we should never store a user password in a java.lang.String because they're immutable. 我担心的是,我们永远不要将用户密码存储在java.lang.String中,因为它们是不可变的。 I can't zero out that String as soon as I'm done authenticating. 完成身份验证后,我无法将String归零。 That object will sit in memory until the garbage collector runs. 该对象将一直位于内存中,直到垃圾回收器运行为止。 That leaves open a much wider window for a bad guy to get a core dump or somehow otherwise observe the heap. 这就为坏人打开了一个更大的窗口,让他可以获取核心转储或以其他方式观察堆。

The problem is that the only way I see to read that Authorization header is via the javax.servlet.http.HttpServletRequest.getHeader(String) method, but it returns a String. 问题是,我看到的读取Authorization标头的唯一方法是通过javax.servlet.http.HttpServletRequest.getHeader(String)方法,但是它返回一个String。 I need a getHeader method that returns an array of bytes or chars. 我需要一个getHeader方法,该方法返回一个字节或字符数组。 Ideally, the request should never be a String at any point in time, from Socket to HttpServletRequest and everywhere in between. 理想情况下,在任何时候,从Socket到HttpServletRequest以及介于两者之间的任何地方,请求都不应为String。

If I switched to some flavor of form-based security, the problem still exists. 如果我改用某种形式的基于表单的安全性,那么问题仍然存在。 javax.servlet.ServletRequest.getParameter(String) returns a String too. javax.servlet.ServletRequest.getParameter(String)返回一个String。

Is this simply a limitation of Java EE? 这仅仅是Java EE的限制吗?

Actually only string literals are keeped in String Pool area of Permgen. 实际上,只有字符串文字保留在Permgen的“字符串池”区域中。 Created Strings are disposables. 创建的字符串是一次性的。

So... Probably memory dump is one of minor problems with Basic Authentication. 所以...内存转储可能是基本身份验证的次要问题之一。 Others are: 其他是:

  • The password is sent over the wire in plaintext. 密码以明文形式通过网络发送。
  • The password is sent repeatedly, for each request. 对于每个请求,密码都会重复发送。 (Larger attack window) (较大的攻击窗口)
  • The password is cached by the webbrowser, at a minimum for the length of the window / process. 密码由网络浏览器缓存,最小长度为窗口/进程的长度。 (Can be silently reused by any other request to the server, eg CSRF). (可以由对服务器的任何其他请求以静默方式重用,例如CSRF)。
  • The password may be stored permanently in the browser, if the user requests. 如果用户要求,密码可以永久存储在浏览器中。 (Same as previous point, in addition might be stolen by another user on a shared machine). (与上一点相同,此外,其他用户可能会在共享计算机上被盗)。
  • Even using SSL, internal servers (behind of SSL protocol) will have access to plain text cacheable password. 即使使用SSL,内部服务器(位于SSL协议之后)也可以访问纯文本可缓存密码。

At the same time, Java container has already parsed HTTP request and populate object. 同时,Java容器已经解析了HTTP请求并填充了对象。 So, that's why you get String from request header. 因此,这就是为什么您从请求标头中获取String的原因。 You probably should rewrite the Web Container to parse safety HTTP request. 您可能应该重写Web容器以解析安全HTTP请求。

Update 更新资料

I was wrong. 我错了。 At least to Apache Tomcat. 至少对于Apache Tomcat。

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

How you can see, BasicAuthenticator from Tomcat project use MessageBytes (ie avoiding String) to perform the authentication. 可以看到,来自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);

}

As long you have access to org.apache.catalina.connector.Request, no worries. 只要您可以访问org.apache.catalina.connector.Request,就不用担心。

So, How can you avoid parsing of HTTP request 因此,如何避免解析HTTP请求

There's an amazing answer here in stackoverflow detailing 在stackoverflow详细信息中有一个了不起的答案

Use servlet filter to remove a form parameter from posted data 使用Servlet过滤器从发布的数据中删除表单参数

and an important explanation: 还有一个重要的解释:

Approach 方法

The code follows the correct approach: 该代码遵循正确的方法:

in wrapRequest(), it instantiates HttpServletRequestWrapper and overrides the 4 methods that trigger request parsing: 在wrapRequest()中,它实例化HttpServletRequestWrapper并覆盖触发请求解析的4种方法:

public String getParameter(String name) public Map getParameterMap() public Enumeration getParameterNames() public String[] getParameterValues(String name) the doFilter() method invokes the filter chain using the wrapped request, meaning subsequent filters, plus the target servlet (URL-mapped) will be supplied the wrapped request. public String getParameter(String name)public Map getParameterMap()public Enumeration getParameterNames()public String [] getParameterValues(String name)doFilter()方法使用包装的请求调用过滤器链,这意味着后续的过滤器以及目标servlet(URL) -mapped)。

没错,但是永远不要在数据库中存储要检查的实际密码,而是对密码本身进行哈希运算,然后运行哈希运算来确定两个哈希值是否相同,即从未使用过但由原始用户使用的密码。

if you are that concerned then use ServletRequest.getInputStream() instead of HttpServletRequest.getHeader(String) in your Filter . 如果您担心的话,请在Filter使用ServletRequest.getInputStream()而不是HttpServletRequest.getHeader(String) You should be able get your HTTP request as a stream, skip till you get to Authorization header and get your password in a char [] . 您应该可以将HTTP请求作为流获取,跳过直到获得Authorization标头并在char []获取密码。

But all this effort might be futile as the underlying object is still a HTTPServletRequest and might contain all the headers as key val pairs in a map, details being subject to how servlet is implemented. 但是,所有这些工作可能都是徒劳的,因为基础对象仍然是HTTPServletRequest并且可能在映射中包含所有标头作为键val对,详细信息取决于servlet的实现方式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM