Samstag, 14. Mai 2011

Apache Tapestry - CSRF Protection - First Prototype

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.

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(
            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:



2 Kommentare:

  1. Hi!
    Thanks for your blog entry, it is really a good tutorial as well.
    Currently I am trying to achieve something similar as well, and your blog helped me a lot. I would like the token be added to the form in any case, without need for annotations, etc. and also be validated without any annotation.
    I already managed the token to be added to every form using a mixin. But now I have troubles with automatic validation, maybe you know a way to do that?

    Thanks!

    AntwortenLöschen
  2. Hi, I cant resolve this error "Failure creating embedded component 'inputs' of foo.bar.registro.pages.inicio.ModEMail: Unable to resolve 'Protected' to a mixin class name."

    AntwortenLöschen