Как безопасно обрабатывать пароли в Java Servlet Filter?

У меня есть фильтр, который обрабатывает ОСНОВНУЮ аутентификацию через HTTPS. Это означает, что приходит заголовок с именем «Авторизация», а значение — что-то вроде «Basic aGVsbG86c3RhY2tvdmVyZmxvdw==".

Меня не волнует, как обрабатывать аутентификацию, заголовок ответа 401 plus WWW-Authenticate, поиск JDBC или что-то в этом роде. Мой фильтр работает прекрасно.

Меня беспокоит то, что мы никогда не должны хранить пароль пользователя в java.lang.String, потому что они неизменны. Я не могу обнулить эту строку, как только завершу аутентификацию. Этот объект будет находиться в памяти до тех пор, пока не запустится сборщик мусора. Это оставляет открытым гораздо более широкое окно для злоумышленника, чтобы получить дамп ядра или как-то иначе наблюдать за кучей.

Проблема в том, что я вижу единственный способ прочитать этот заголовок авторизации с помощью метода javax.servlet.http.HttpServletRequest.getHeader(String), но он возвращает строку. Мне нужен метод getHeader, который возвращает массив байтов или символов. В идеале запрос никогда не должен быть строкой в ​​любой момент времени, от Socket до HttpServletRequest и везде между ними.

Если бы я переключился на какую-то разновидность безопасности на основе форм, проблема все еще существовала бы. javax.servlet.ServletRequest.getParameter(String) тоже возвращает строку.

Это просто ограничение Java EE?


person bmauter    schedule 12.02.2014    source источник


Ответы (3)


На самом деле в области пула строк Permgen хранятся только строковые литералы. Созданные строки являются одноразовыми.

Итак... Вероятно, дамп памяти - одна из мелких проблем с базовой аутентификацией. Другие:

  • Пароль передается по сети в открытом виде.
  • Пароль отправляется повторно, для каждого запроса. (Увеличенное окно атаки)
  • Пароль кэшируется веб-браузером, как минимум на длину окна/процесса. (Может быть повторно использован любым другим запросом к серверу, например, CSRF).
  • Пароль может постоянно храниться в браузере, если пользователь этого потребует. (То же, что и в предыдущем пункте, кроме того, он может быть украден другим пользователем на общей машине).
  • Даже при использовании SSL внутренние серверы (за протоколом SSL) будут иметь доступ к кэшируемому паролю в виде обычного текста.

В то же время контейнер Java уже проанализировал HTTP-запрос и заполнил объект. Итак, вот почему вы получаете String из заголовка запроса. Вероятно, вам следует переписать веб-контейнер для анализа безопасного HTTP-запроса.

Обновлять

Я ошибался. По крайней мере, для Apache Tomcat.

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

Как видите, BasicAuthenticator из проекта Tomcat использует 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);

}

Пока у вас есть доступ к org.apache.catalina.connector.Request, не беспокойтесь.

Итак, как вы можете избежать разбора HTTP-запроса

Здесь есть удивительный ответ в детализации stackoverflow

Используйте фильтр сервлета, чтобы удалить параметр формы из опубликованного данные

и важное пояснение:

Подход

Код следует правильному подходу:

в wrapRequest() он создает экземпляр HttpServletRequestWrapper и переопределяет 4 метода, запускающих синтаксический анализ запроса:

public String getParameter(String name) public Map getParameterMap() public Enumeration getParameterNames() public String[] getParameterValues(String name) метод doFilter() вызывает цепочку фильтров, используя обернутый запрос, то есть последующие фильтры плюс целевой сервлет (URL -mapped) будет предоставлен обернутый запрос.

person rdllopes    schedule 12.02.2014
comment
Я признаю ограничения Basic Auth. Однако при использовании HTTPS пароль не передается по сети в открытом виде. HTTPS предотвращает подслушивание, поэтому, надеюсь, соединение между браузером пользователя и моим сервером является конфиденциальным. На моем сервере работает фильтр сервлетов. - person bmauter; 12.02.2014
comment
Да. Ты прав. Пароль не передается по сети в открытом виде. Но на сервере приложений Tomcat по умолчанию пароль пользователя хранится в виде открытого текста для каждой из стандартных реализаций Realm. Иногда вы также можете использовать сервер Apache перед сервером Tomcat с разъемом ajp. Наконец, если вы используете Tomcat напрямую, HTTPS предотвратит прослушивание. - person rdllopes; 13.02.2014
comment
Ладно, я понимаю, почему ты это указал. У нас уже есть пользовательская таблица в нашем приложении с хешированным полем пароля. Мой фильтр сервлетов аутентифицируется против него. - person bmauter; 13.02.2014
comment
Я не думаю, что мне нужно беспокоиться об упаковке запроса. Я мог бы использовать метод request.getCoyoteRequest.getMimeHeaders().getValue(String), чтобы получить char[] для пароля, а затем, возможно, даже обнулить его после того, как я выполнил аутентификацию. Я не в восторге от привязки его к Tomcat, так как у нас есть клиенты, которые используют разные веб-контейнеры. Я мог бы использовать метод getHeader(String) по умолчанию, если бы не Tomcat, и этот сумасшедший способ, когда я нахожусь в Tomcat. Ух ты. - person bmauter; 13.02.2014
comment
Спасибо @rdllopes. В конечном счете, мы не собираемся помещать в наши сервлеты вещи, зависящие от поставщика, даже если это Tomcat. Я считаю, что на мой вопрос ответили, поскольку вы подтвердили, что J2EE не предоставляет механизма для этого, и потому что вы предложили обходной путь. Мы можем вернуться к этому однажды, кто знает. - person bmauter; 14.02.2014

Это правильно, но он никогда не должен хранить в БД фактический пароль для проверки, а вместо этого хеш на самом пароле, а затем запускать хеш, чтобы определить, идентичны ли два хэша, которые пароль никогда не используется, но первоначальным пользователем.

person user2991535    schedule 12.02.2014
comment
Меня не интересует бит JDBC (второй абзац исходного вопроса). Я использую хэш bcrypt. - person bmauter; 12.02.2014

если вас это беспокоит, используйте ServletRequest.getInputStream() вместо HttpServletRequest.getHeader(String) в своем Filter. Вы должны получить свой HTTP-запрос в виде потока, пропустить, пока не дойдете до заголовка Authorization, и получить свой пароль в файле char [].

Но все эти усилия могут оказаться бесполезными, поскольку базовый объект по-прежнему является HTTPServletRequest и может содержать все заголовки в виде пар ключ-значение на карте, причем детали зависят от того, как реализован сервлет.

person mzzzzb    schedule 12.02.2014
comment
К сожалению, getInputStream возвращает только тело запроса. Заголовки располагаются перед телом. Однако вы правы, контейнер может сделать это бесполезным (таким образом, мой комментарий о запросе от Socket к HttpServletRequest в исходном вопросе). - person bmauter; 12.02.2014
comment
мой плохой просто поместил мой мыслительный процесс в этот ответ, не проверяя его - person mzzzzb; 14.02.2014