To get in touch with tapestry internals I implemented a first draft of a CSRF protection mechanism based on a mixin and an annotation named Protected.
The mixin adds a hidden formtoken input field into a Form component - currently in a really ugly way. Additionally the formtoken is also stored in the session at the server side.
The magic happens in the ProtectedWorker class, where the class transformation is done with the help of Plastic. The methods that have the @Protected annotation are enhanced with the comparison of the session parameter and the request parameter. In the case of an CSRF attack an exception is thrown.
To make it run, the class transform configuratoin in the AppModule is required.
I just added the mixin in the html template of my sample applications and protected the onSuccess event handler.
The mixin adds a hidden formtoken input field into a Form component - currently in a really ugly way. Additionally the formtoken is also stored in the session at the server side.
Protected - Mixin
package org.apache.tapestry5.csrfprotection.victimapp.mixins; import java.util.List; import java.util.UUID; import org.apache.tapestry5.MarkupWriter; import org.apache.tapestry5.dom.Element; import org.apache.tapestry5.dom.Node; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.services.Request; public class Protected { @Inject private Request request; void afterRender(MarkupWriter writer) { List<Node> nodes = writer.getElement().getChildren(); for(Node node:nodes){ if(node instanceof Element && ((Element)node).getName().equals("form")){ Element form = (Element) node; String token = UUID.randomUUID().toString(); form.element("input", "type","hidden","name","formtoken","value",token); request.getSession(true).setAttribute("formtoken", token); } } } }The value of the formtoken stored in the session is compared against the HTTP request parameter, if the event handler is annotated with the Protected annotation.
Protected - Annotation
package org.apache.tapestry5.csrfprotection.victimapp.annotations; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target(METHOD) @Retention(RUNTIME) @Documented public @interface Protected { }
The magic happens in the ProtectedWorker class, where the class transformation is done with the help of Plastic. The methods that have the @Protected annotation are enhanced with the comparison of the session parameter and the request parameter. In the case of an CSRF attack an exception is thrown.
ProtectedWorker
package org.apache.tapestry5.csrfprotection.victimapp.protection; import org.apache.tapestry5.csrfprotection.victimapp.annotations.Protected; import org.apache.tapestry5.model.MutableComponentModel; import org.apache.tapestry5.plastic.MethodAdvice; import org.apache.tapestry5.plastic.PlasticClass; import org.apache.tapestry5.plastic.PlasticMethod; import org.apache.tapestry5.services.Request; import org.apache.tapestry5.services.Session; import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; import org.apache.tapestry5.services.transform.TransformationSupport; import org.apache.tapestry5.plastic.MethodInvocation; public class ProtectedWorker implements ComponentClassTransformWorker2 { private Request request; public ProtectedWorker(Request request){ this.request = request; } public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) { final MethodAdvice myLoggingAdvice = new MethodAdvice() { public void advise(MethodInvocation invocation) { String requestParam = request.getParameter("formtoken"); String sessionParam = ""; // avoid null pointer exceptions Session session = request.getSession(false); if(session != null){ sessionParam = session.getAttribute("formtoken").toString(); } if(sessionParam.equals(requestParam)){ invocation.proceed(); } else{ invocation.setCheckedException(new ProtectionException("CSRF Attack detected.")); invocation.rethrow(); } } }; for (final PlasticMethod method : plasticClass .getMethodsWithAnnotation(Protected.class)) { method.addAdvice(myLoggingAdvice); } } }The exception class is currently trivial.
ProtectedException
package org.apache.tapestry5.csrfprotection.victimapp.protection; public class ProtectedException extends Exception { public ProtectedException(String msg){ super(msg); } }
To make it run, the class transform configuratoin in the AppModule is required.
AppModule - Fragment
@Contribute(ComponentClassTransformWorker2.class) public static void provideTransformWorkers( OrderedConfigurationconfiguration, MetaWorker metaWorker, ComponentClassResolver resolver) { configuration.addInstance("Protected",ProtectedWorker.class); }
I just added the mixin in the html template of my sample applications and protected the onSuccess event handler.
Usage
<form t:type="form" t:id="statusForm" t:mixins="Protected">
@Protected private Object onSuccess(){ history.getHistory().add(statusMessage); return this; }