
Otro problema de seguridad es CSRF (Cross-site request forgery) en el que básicamente un sitio al que se accede devuelve un enlace malicioso que provoca una acción en otro, el atacado. El enlace devuelto puede producir cualquier acción que el sitio atacado permita, el ejemplo que se suele poner es el de un sitio bancario y el intento de hacer una transferencia de la cuenta del usuario que tiene iniciada una sesión en la página de su banco a la cuenta del atacante pero podría ser la realización de un cambio de contraseña a una que conozca el atacante y de esta forma posteriormente este pueda autenticarse con la cuenta de ese usuario en el sitio atacado. En la wikipedia este problema de seguridad está más ampliamente explicado con ejemplos, limitaciones y como prevenirlo. A diferencia de XSS donde el usuario confia en lo que obtiene del servidor en el caso de CSRF es al contrario, el servidor confia en las peticiones del cliente, aunque puedan provenir de un sitio malicioso.
Una solución que suele aplicarse para resolver el problema de CSRF es incluir en los enlaces y formularios un token de seguridad, si el token no se envía o no se corresponde con el token del servidor la petición se considera inválida y no se procesa. Al acceder a una página se genera el token de seguridad y se incluye en todos los enlaces y formularios, este token no será conocido por un a tercera parte y los enlaces maliciosos no serán procesados.
En esta entrada mostraré como solucionar este problema en Tapestry con una combinación de mixin, anotación, advice y objeto de estado de aplicación (SSO), similar a lo explicado en este blog pero con la adición que no solo sirve para formularios sino también para enlaces y el componente BeanEditForm.
Primero veamos el objeto estado de aplicación que contendrá el token (sid) de seguridad, lo generará y lo validará, este objeto de estado de aplicación se guardará a nivel de sesión de modo que el token que se envía en la petición pueda ser validado con el token guardado en este SSO.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.sso; | |
import java.io.Serializable; | |
import java.util.UUID; | |
public class Sid implements Serializable { | |
private static final long serialVersionUID = -4552333438930728660L; | |
private String sid; | |
protected Sid(String sid) { | |
this.sid = sid; | |
} | |
public static Sid newInstance() { | |
return new Sid(UUID.randomUUID().toString()); | |
} | |
public String getSid() { | |
return sid; | |
} | |
public boolean isValid(String sid) { | |
return this.sid.equals(sid); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.mixins; | |
import org.apache.tapestry5.ComponentResources; | |
import org.apache.tapestry5.MarkupWriter; | |
import org.apache.tapestry5.annotations.InjectContainer; | |
import org.apache.tapestry5.annotations.MixinAfter; | |
import org.apache.tapestry5.annotations.SessionState; | |
import org.apache.tapestry5.corelib.components.ActionLink; | |
import org.apache.tapestry5.corelib.components.BeanEditForm; | |
import org.apache.tapestry5.corelib.components.EventLink; | |
import org.apache.tapestry5.corelib.components.Form; | |
import org.apache.tapestry5.dom.Element; | |
import org.apache.tapestry5.dom.Node; | |
import org.apache.tapestry5.ioc.annotations.Inject; | |
import org.apache.tapestry5.runtime.Component; | |
import org.apache.tapestry5.services.Request; | |
import es.com.blogspot.elblogdepicodev.plugintapestry.services.sso.Sid; | |
@MixinAfter | |
public class Csrf { | |
@SessionState(create = false) | |
private Sid sid; | |
@Inject | |
private Request request; | |
@Inject | |
private ComponentResources resources; | |
@InjectContainer | |
private Component container; | |
void beginRender(MarkupWriter writer) { | |
if (container instanceof EventLink || container instanceof ActionLink) { | |
buildSid(); | |
Element element = writer.getElement(); | |
String href = element.getAttribute("href"); | |
String character = (href.indexOf('?') == -1) ? "?" : "&"; | |
element.forceAttributes("href", String.format("%s%st:sid=%s", href, character, sid.getSid())); | |
} | |
} | |
void afterRenderTemplate(MarkupWriter writer) { | |
if (container instanceof BeanEditForm) { | |
Element form = null; | |
for (Node node : writer.getElement().getChildren()) { | |
if (node instanceof Element) { | |
Element element = (Element) node; | |
if (element.getName().equals("form")) { | |
form = element; | |
break; | |
} | |
} | |
} | |
if (form != null) { | |
buildSid(); | |
Element e = form.element("input", "type", "hidden", "name", "t:sid", "value", sid.getSid()); | |
e.moveToTop(form); | |
} | |
} | |
} | |
void beforeRenderBody(MarkupWriter writer) { | |
if (container instanceof Form) { | |
buildSid(); | |
Element form = (Element) writer.getElement(); | |
form.element("input", "type", "hidden", "name", "t:sid", "value", sid.getSid()); | |
} else if (container instanceof BeanEditForm) { | |
buildSid(); | |
Element form = (Element) writer.getElement(); | |
form.element("input", "type", "hidden", "name", "t:sid", "value", sid.getSid()); | |
} | |
} | |
private void buildSid() { | |
if (sid == null) { | |
sid = Sid.newInstance(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.annotation; | |
import java.lang.annotation.Documented; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.RUNTIME) | |
@Documented | |
public @interface Csrf { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.services.workers; | |
import org.apache.tapestry5.model.MutableComponentModel; | |
import org.apache.tapestry5.plastic.MethodAdvice; | |
import org.apache.tapestry5.plastic.MethodInvocation; | |
import org.apache.tapestry5.plastic.PlasticClass; | |
import org.apache.tapestry5.plastic.PlasticMethod; | |
import org.apache.tapestry5.services.ApplicationStateManager; | |
import org.apache.tapestry5.services.Request; | |
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; | |
import org.apache.tapestry5.services.transform.TransformationSupport; | |
import es.com.blogspot.elblogdepicodev.plugintapestry.services.annotation.Csrf; | |
import es.com.blogspot.elblogdepicodev.plugintapestry.services.exceptions.CSRFException; | |
import es.com.blogspot.elblogdepicodev.plugintapestry.services.sso.Sid; | |
public class CsrfWorker implements ComponentClassTransformWorker2 { | |
private Request request; | |
private ApplicationStateManager manager; | |
public CsrfWorker(Request request, ApplicationStateManager manager) { | |
this.request = request; | |
this.manager = manager; | |
} | |
public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) { | |
MethodAdvice advice = new MethodAdvice() { | |
public void advise(MethodInvocation invocation) { | |
String rsid = request.getParameter("t:sid"); | |
Sid sid = manager.getIfExists(Sid.class); | |
if (sid != null && sid.isValid(rsid)) { | |
invocation.proceed(); | |
} else { | |
invocation.setCheckedException(new CSRFException("El parámetro sid de la petición no se corresponde con el sid de la sesión. Esta petición no es válida (Posible ataque CSRF).")); | |
invocation.rethrow(); | |
} | |
} | |
}; | |
for (PlasticMethod method : plasticClass.getMethodsWithAnnotation(Csrf.class)) { | |
method.addAdvice(advice); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Contribute(ComponentClassTransformWorker2.class) | |
public static void contributeWorkers(OrderedConfiguration<ComponentClassTransformWorker2> configuration) { | |
configuration.addInstance("CSRF", CsrfWorker.class); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h4>Solución al CSRF</h4> | |
<p> | |
Cuenta: <t:zone t:id="csrfZone" id="csrfZone" elementName="span">${cuenta}</t:zone> | |
<div class="row"> | |
<div class="col-md-4"> | |
<h5>En formulario</h5> | |
<form t:id="csrfForm" t:type="form" t:zone="csrfZone" t:mixins="csrf"> | |
<input t:type="submit" value="Sumar 1"/> | |
</form> | |
</div> | |
<div class="col-md-4"> | |
<h5>En enlace</h5> | |
<a t:type="eventlink" t:event="sumar1CuentaCsrf" t:zone="csrfZone" t:mixins="csrf">Sumar 1</a> | |
</div> | |
<div class="col-md-4"> | |
<h5>Fallo seguridad</h5> | |
<a t:type="eventlink" t:event="sumar1CuentaCsrf" t:zone="csrfZone" t:parameters="prop:{'t:sid':'dummy-attack'}">Sumar 1</a> | |
</div> | |
</div> | |
</p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Csrf | |
void onSuccessFromCsrfForm() { | |
cuenta += 1; | |
renderer.addRender("zone", zone).addRender("submitOneZone", submitOneZone).addRender("csrfZone", csrfZone); | |
} | |
@Csrf | |
void onSumar1CuentaCsrf() { | |
cuenta += 1; | |
renderer.addRender("zone", zone).addRender("submitOneZone", submitOneZone).addRender("csrfZone", csrfZone); | |
} |
Esta es la sección de la aplicación del ejemplo funcionando donde puede probarse el mixin y ver la diferencia del comportamiento sin el mixin aplicado.
Cuando pulsamos en el enlace que envía un t:sid inválido mediante un petición ajax provocará el siguiente informe de error con un mensaje descriptivo de lo que ha ocurrido.
En las siguientes imágenes puede verse el parámetro t:sid que se añade tanto a un formulario como a un enlace cuando le aplicamos el mixin csrf.
![]() |
Parámetro t:sid en un formulario |
![]() |
Parámetro t:sid en un enlace |
¿Ya tienes en cuenta el problema de seguridad CSRF en tus aplicaciones web? ¿Con el framework que uses aplicar una solución es tan simple como esta?
De esta entrada aparte de resolver el problema de seguridad CSRF quiero destacar con se realiza la metaprogramación en Apache Tapestry con las clases Plastic de Tapestry en la clase CsrfWorker.
Si te ha parecido interesante esta entrada puedes descargar el libro PlugIn Tapestry en el que explico más en detalle como desarrollar aplicaciones web en Tapestry y en el que descubrirás como resolver problemas comunes como este en las aplicaciones web de una forma tan buena como esta.
Si quieres probarlo en tu equipo lo puedes hacer de forma muy sencilla con los siguientes comandos y sin instalar nada. Si no dispones de git para clonar mi repositorio de GitHub puedes obtener el código fuente del repositorio en un archivo zip.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ git clone git://github.com/picodotdev/elblogdepicodev.git | |
$ cd elblogdepicodev/PlugInTapestry | |
$ ./gradlew tomcatRun | |
# Abrir en el navegador http://localhost:8080/PlugInTapestry/ |
Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet
Apache Tapestry - CSRF Protection - First Prototype
http://wiki.apache.org/tapestry/Tapestry5CSRF
https://code.google.com/p/gsoc2011-csrf-protection