Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a filter that handles BASIC authentication over HTTPS. That means there's a header coming in named "Authorization" and the value is something like "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. My filter works beautifully.

My concern is that we should never store a user password in a java.lang.String because they're immutable. I can't zero out that String as soon as I'm done authenticating. 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. I need a getHeader method that returns an array of bytes or chars. Ideally, the request should never be a String at any point in time, from Socket to HttpServletRequest and everywhere in between.

If I switched to some flavor of form-based security, the problem still exists. javax.servlet.ServletRequest.getParameter(String) returns a String too.

Is this simply a limitation of Java EE?

like image 216
bmauter Avatar asked Oct 01 '22 14:10

bmauter


1 Answers

Actually only string literals are keeped in String Pool area of 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, e.g. 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.

At the same time, Java container has already parsed HTTP request and populate object. So, that's why you get String from request header. You probably should rewrite the Web Container to parse safety HTTP request.

Update

I was wrong. At least to Apache Tomcat.

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 (i.e. avoiding String) to perform the authentication.

/**
 * 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.

So, How can you avoid parsing of HTTP request

There's an amazing answer here in stackoverflow detailing

Use servlet filter to remove a form parameter from posted data

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:

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.

like image 113
rdllopes Avatar answered Oct 06 '22 07:10

rdllopes