Search code examples
jsf-2csrfviewstatejsf-2.2myfaces

Prevent CSRF in JSF2 with client side state saving


I'm Using MyFaces 2.2.3 with client side state saving + PrimeFaces

After asking how to prevent the re-use of a ViewState in different sessions I was told by BalusC , that I can inject my own CSRF token by override the from renderer to let the value be a CSRF token ,

I'm looking for a solution that wont force me to modify my xhtml pages at all :)


BalusC has suggested a better way to prevent CSRF attack by extending ViewHandlerWrapper , and it works great, I only had to modify a bit the restoreView in the following way

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;
    }
}

Old solution

I tried without success so far,

Added to 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>   

Then in 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...
}

I don't want to add a custom component to each and every h:form, instead I want to extend the form renderer so all my form will have the csrf token.


Solution

  • This <h:form> renderer override approach is not safe to PrimeFaces partialSubmit="true". Also, reusing its hidden field identifying the submitted form would be JSF implementation specific as this is not part of JSF API.

    On a second thought, it's much simpler to store the CSRF token just directly in the JSF view state itself. You can achieve that with a custom ViewHandler as below which sets an attribute in UIViewRoot (which get automatically saved in JSF view state):

    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;
        }
    
    }
    

    Do note that when restoreView() returns null, JSF will throw a ViewExpiredException "as usual".

    To get it to run, register as below in faces-config.xml:

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

    Because it has no additional value with server side state saving, you can if necessary detect as below in the constructor of the view handler if the current JSF application is configured with client side state saving:

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