JSF 2.x has already builtin CSRF prevention in flavor of javax.faces.ViewState hidden field in the form when using server side state saving. In JSF 1.x this value was namely pretty weak and too easy predictable (it was actually never intended as CSRF prevention). In JSF 2.0 this has been improved by using a long and strong autogenerated value instead of a rather predictable sequence value and thus making it a robust CSRF prevention.
In JSF 2.2 this is even be further improved by making it a required part of the JSF specification, along with a configurable AES key to encrypt the client side state, in case client side state saving is enabled. New in JSF 2.2 is CSRF protection on GET requests by <protected-views>.
Using the legacy code of JSF 1.x, you can extend the form and add a token to the form. The token is generated by the SessionListener and saved in the Session. The code is as follows:
CsrfSessionListener
public class CsrfSessionListener implements HttpSessionListener { private static final String CSRF_TOKEN_NAME = "CsrfToken"; @Override public void sessionCreated(HttpSessionEvent se) { HttpSession session = se.getSession(); session.setAttribute(CSRF_TOKEN_NAME, UUID.randomUUID().toString()); } @Override public void sessionDestroyed(HttpSessionEvent se) { } }
CsrfForm
public class CsrfForm extends HtmlForm { private static final String CSRF_TOKEN_NAME = "CsrfToken"; private static final String CSRF_TOKEN_ID = "csrf_token"; @Override public void encodeEnd(FacesContext context) throws IOException { encodeCsrfTokenInput(context); super.encodeEnd(context); } private void encodeCsrfTokenInput(FacesContext context) throws IOException { ResponseWriter responseWriter = context.getResponseWriter(); responseWriter.startElement("input", null); responseWriter.writeAttribute("type", "hidden", null); responseWriter.writeAttribute("id", getCsrfInputClientId(context), null); responseWriter.writeAttribute("name", CSRF_TOKEN_ID, null); responseWriter.writeAttribute("value", getToken(context), "value"); responseWriter.endElement("input"); } @Override public void decode(FacesContext context) { Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap(); String value = requestMap.get(CSRF_TOKEN_ID); // check if the token exists if (value == null || "null".equals(value) || "".equals(value)) { throw new CsrfException("CSRFToken is missing!"); } String token = getToken(context); // check the values for equality if (!value.equalsIgnoreCase(token)) { throw new CsrfException("CSRFToken does not match!"); } super.decode(context); } private String getCsrfInputClientId(FacesContext context) { return getClientId(context) + ":" + CSRF_TOKEN_ID; } private String getToken(FacesContext context) { HttpSession session = (HttpSession) context.getExternalContext().getSession(false); return (String) session.getAttribute(CSRF_TOKEN_NAME); } }
Then add configuration in faces-config.xml and use CsrfForm:
<component> <component-type>javax.faces.HtmlForm</component-type> <component-class>org.iata.csrf.CsrfForm</component-class> </component>