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
.
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.
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(
OrderedConfiguration configuration,
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;
}
CSRF - Attack
The sample attack that targets the form submit fails now with an exception: