Предотвращение CSRF в JSF2 с сохранением состояния на стороне клиента


Я использую MyFaces 2.2.3 с сохранением состояния на стороне клиента + PrimeFaces

После того, как я спросил , как предотвратить повторное использование ViewState в различных сеансах, Мнесказал BalusC , Что я могу ввести мой собственный CSRF-токен с помощью переопределить отрисовщик, чтобы значение было CSRF-токеном,

Я ищу решение, которое не заставит меня изменять моиxhtml страницы вообще :)


BalusC имеет предложил лучший способ предотвратить атаку CSRF путем расширения ViewHandlerWrapper, и это отлично работает, мне только пришлось немного изменить restoreView следующим образом

public UIViewRoot restoreView(FacesContext context, String viewId) {
    UIViewRoot view = super.restoreView(context, viewId);
    if (getCsrfToken(context).equals(view.getAttributes().get(CSRF_TOKEN_KEY))) {
        return view;
    } else {
        HttpSession session = (HttpSession) context.getExternalContext().getSession(false);
        if (session != null) {
            session.invalidate(); //invalidate session so (my custom and unrelated) PhaseListener will notice that its a bad session now
        }
        try {
            FacesContext.getCurrentInstance().getExternalContext().redirect("CSRF detected and blocked"); //better looking user feedback
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Старое решение

До сих пор я безуспешно пытался,

Добавлено в faces-config.xml

<render-kit>
    <renderer>
        <component-family>javax.faces.Form</component-family>
        <renderer-type>javax.faces.Form</renderer-type>
        <renderer-class>com.communitake.mdportal.renderers.CTFormRenderer</renderer-class>
    </renderer>
</render-kit>   

Затем в CTFormRenderer.java

@Override
public void encodeEnd(FacesContext context, UIComponent arg1) throws IOException {
    //how to set form value be a CSRF token?
}

@Override
public void decode(FacesContext context, UIComponent component) {
    HttpSession session = (HttpSession) context.getExternalContext().getSession(false);
    String token = (String) session.getAttribute(CSRFTOKEN_NAME);
    String tokenFromForm = //how to get the value stored in form value attribute because (String) component.getAttributes().get("value"); return null
    //check token against tokenFromForm...
}

Я не хочу добавлять пользовательский компонент к каждому h:form, вместо этого я хочу расширить form визуализатор, чтобы вся моя форма имела csrf token.

1 4

1 ответ:

Этот подход переопределения рендера <h:form> небезопасен для примитивов partialSubmit="true". Кроме того, повторное использование его скрытого поля, идентифицирующего представленную форму, было бы специфичной реализацией JSF, поскольку это не является частью API JSF.

С другой стороны, гораздо проще хранить маркер CSRF непосредственно в самом состоянии представления JSF. Вы можете достичь этого с помощью пользовательского ViewHandler , как показано ниже, который устанавливает атрибут в UIViewRoot (который автоматически сохраняется в состоянии представления JSF):

public class CsrfViewHandler extends ViewHandlerWrapper {

    private static final String CSRF_TOKEN_KEY = CsrfViewHandler.class.getName();

    private ViewHandler wrapped;

    public CsrfViewHandler(ViewHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public UIViewRoot restoreView(FacesContext context, String viewId) {
        UIViewRoot view = super.restoreView(context, viewId);
        return getCsrfToken(context).equals(view.getAttributes().get(CSRF_TOKEN_KEY)) ? view : null;
    }

    @Override
    public void renderView(FacesContext context, UIViewRoot view) throws IOException, FacesException {
        view.getAttributes().put(CSRF_TOKEN_KEY, getCsrfToken(context));
        super.renderView(context, view);
    }

    private String getCsrfToken(FacesContext context) {
        String csrfToken = (String) context.getExternalContext().getSessionMap().get(CSRF_TOKEN_KEY);

        if (csrfToken == null) {
            csrfToken = UUID.randomUUID().toString();
            context.getExternalContext().getSessionMap().put(CSRF_TOKEN_KEY, csrfToken);
        }

        return csrfToken;
    }

    @Override
    public ViewHandler getWrapped() {
        return wrapped;
    }

}

Обратите внимание, что когда restoreView() возвращает null, JSF бросит ViewExpiredException "как обычно".

Чтобы запустить его, зарегистрируйтесь, как показано ниже в faces-config.xml:

<application>
    <view-handler>com.example.CsrfViewHandler</view-handler>    
</application>

Поскольку он не имеет дополнительного значения с сохранением состояния на стороне сервера, вы можете при необходимости определить, как показано ниже в конструкторе обработчика представления, если текущее приложение JSF настроено с сохранением состояния на стороне клиента:

FacesContext context = FacesContext.getCurrentInstance();

if (!context.getApplication().getStateManager().isSavingStateInClient(context)) {
    throw new IllegalStateException("This view handler is only applicable when JSF is configured with "
        + StateManager.STATE_SAVING_METHOD_PARAM_NAME + "=" + StateManager.STATE_SAVING_METHOD_CLIENT);
}