Una guía para la protección CSRF en Spring Security

1. Información general

En este tutorial, discutiremos los ataques CSRF de falsificación de solicitudes entre sitios y cómo prevenirlos usando Spring Security.

2. Dos ataques CSRF simples

Existen múltiples formas de ataques CSRF; analicemos algunas de las más comunes.

2.1. GET Ejemplos

Consideremos la siguiente solicitud GET utilizada por usuarios registrados para transferir dinero a una cuenta bancaria específica "1234" :

GET //bank.com/transfer?accountNo=1234&amount=100

Si el atacante desea transferir dinero de la cuenta de una víctima a su propia cuenta, "5678" , debe hacer que la víctima active la solicitud:

GET //bank.com/transfer?accountNo=5678&amount=1000

Hay varias formas de lograrlo:

  • Enlace: el atacante puede convencer a la víctima de que haga clic en este enlace, por ejemplo, para ejecutar la transferencia:
 Show Kittens Pictures 
  • Imagen: el atacante puede utilizar unetiqueta con la URL de destino como fuente de la imagen, por lo que el clic ni siquiera es necesario. La solicitud se ejecutará automáticamente cuando se cargue la página:

2.2. Ejemplo de POST

Si la solicitud principal debe ser una solicitud POST, por ejemplo:

POST //bank.com/transfer accountNo=1234&amount=100

Luego, el atacante necesita que la víctima ejecute un procedimiento similar:

POST //bank.com/transfer accountNo=5678&amount=1000

Ni el o la funcionará en este caso. El atacante necesitará un - como sigue:

Sin embargo, el formulario se puede enviar automáticamente usando Javascript, de la siguiente manera:

  ...

2.3. Simulación práctica

Ahora que entendemos cómo se ve un ataque CSRF, simulemos estos ejemplos dentro de una aplicación Spring.

Comenzaremos con una implementación de controlador simple, el BankController :

@Controller public class BankController { private Logger logger = LoggerFactory.getLogger(getClass()); @RequestMapping(value = "/transfer", method = RequestMethod.GET) @ResponseBody public String transfer(@RequestParam("accountNo") int accountNo, @RequestParam("amount") final int amount) { logger.info("Transfer to {}", accountNo); ... } @RequestMapping(value = "/transfer", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void transfer2(@RequestParam("accountNo") int accountNo, @RequestParam("amount") final int amount) { logger.info("Transfer to {}", accountNo); ... } }

Y también tengamos una página HTML básica que activa la operación de transferencia bancaria:

 Transfer Money to John  Account Number  Amount     

Esta es la página de la aplicación principal, que se ejecuta en el dominio de origen.

Tenga en cuenta que hemos simulado tanto un GET a través de un enlace simple como un POST a través de un simple.

Ahora, veamos cómo se vería la página del atacante :

  Show Kittens Pictures 

Esta página se ejecutará en un dominio diferente: el dominio del atacante.

Finalmente, ejecutemos las dos aplicaciones, la aplicación original y la del atacante, localmente, y accedamos primero a la página original:

//localhost:8081/spring-rest-full/csrfHome.html

Luego, accedamos a la página del atacante:

//localhost:8081/spring-security-rest/api/csrfAttacker.html

Al rastrear las solicitudes exactas que se originan en esta página del atacante, podremos detectar inmediatamente la solicitud problemática, presionando la aplicación original y completamente autenticada.

3. Configuración de seguridad de primavera

Para usar la protección CSRF de Spring Security, primero debemos asegurarnos de usar los métodos HTTP adecuados para cualquier cosa que modifique el estado ( PATCH , POST , PUT y DELETE, no GET).

3.1. Configuración de Java

La protección CSRF está habilitada de forma predeterminada en la configuración de Java. Aún podemos deshabilitarlo si necesitamos:

@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable(); }

3.2. Configuración XML

En la configuración XML anterior (anterior a Spring Security 4), la protección CSRF estaba deshabilitada de forma predeterminada y podríamos habilitarla de la siguiente manera:

 ...  

A partir de Spring Security 4.x , la protección CSRF también está habilitada de forma predeterminada en la configuración XML; por supuesto, aún podemos deshabilitarlo si necesitamos:

 ...  

3.3. Parámetros de forma adicional

Finalmente, con la protección CSRF habilitada en el lado del servidor, también necesitaremos incluir el token CSRF en nuestras solicitudes en el lado del cliente:

3.4. Usando JSON

No podemos enviar el token CSRF como parámetro si usamos JSON; en su lugar, podemos enviar el token dentro del encabezado.

Primero necesitaremos incluir el token en nuestra página, y para eso, podemos usar metaetiquetas:

Luego construiremos el encabezado:

var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(e, xhr, options) { xhr.setRequestHeader(header, token); });

4. Prueba de CSRF desactivado

Con todo eso en su lugar, pasaremos a hacer algunas pruebas.

Primero intentemos enviar una solicitud POST simple cuando CSRF está deshabilitado:

@ContextConfiguration(classes = { SecurityWithoutCsrfConfig.class, ...}) public class CsrfDisabledIntegrationTest extends CsrfAbstractIntegrationTest { @Test public void givenNotAuth_whenAddFoo_thenUnauthorized() throws Exception { mvc.perform( post("/foos").contentType(MediaType.APPLICATION_JSON) .content(createFoo()) ).andExpect(status().isUnauthorized()); } @Test public void givenAuth_whenAddFoo_thenCreated() throws Exception { mvc.perform( post("/foos").contentType(MediaType.APPLICATION_JSON) .content(createFoo()) .with(testUser()) ).andExpect(status().isCreated()); } }

Como habrás notado, estamos usando una clase base para contener la lógica auxiliar de prueba común : CsrfAbstractIntegrationTest :

@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration public class CsrfAbstractIntegrationTest { @Autowired private WebApplicationContext context; @Autowired private Filter springSecurityFilterChain; protected MockMvc mvc; @Before public void setup() { mvc = MockMvcBuilders.webAppContextSetup(context) .addFilters(springSecurityFilterChain) .build(); } protected RequestPostProcessor testUser() { return user("user").password("userPass").roles("USER"); } protected String createFoo() throws JsonProcessingException { return new ObjectMapper().writeValueAsString(new Foo(randomAlphabetic(6))); } }

Tenga en cuenta que, cuando el usuario tenía las credenciales de seguridad adecuadas, la solicitud se ejecutó correctamente, no se requirió información adicional.

Eso significa que el atacante puede simplemente usar cualquiera de los vectores de ataque discutidos anteriormente para comprometer fácilmente el sistema.

5. Prueba habilitada para CSRF

Ahora, habilitemos la protección CSRF y veamos la diferencia:

@ContextConfiguration(classes = { SecurityWithCsrfConfig.class, ...}) public class CsrfEnabledIntegrationTest extends CsrfAbstractIntegrationTest { @Test public void givenNoCsrf_whenAddFoo_thenForbidden() throws Exception { mvc.perform( post("/foos").contentType(MediaType.APPLICATION_JSON) .content(createFoo()) .with(testUser()) ).andExpect(status().isForbidden()); } @Test public void givenCsrf_whenAddFoo_thenCreated() throws Exception { mvc.perform( post("/foos").contentType(MediaType.APPLICATION_JSON) .content(createFoo()) .with(testUser()).with(csrf()) ).andExpect(status().isCreated()); } }

Ahora, cómo esta prueba usa una configuración de seguridad diferente, una que tiene la protección CSRF habilitada.

Ahora, la solicitud POST simplemente fallará si el token CSRF no está incluido, lo que por supuesto significa que los ataques anteriores ya no son una opción.

Finalmente, observe el método csrf () en la prueba; esto crea un RequestPostProcessor que automáticamente completará un token CSRF válido en la solicitud con fines de prueba.

6. Conclusión

En este artículo, discutimos un par de ataques CSRF y cómo prevenirlos usando Spring Security.

La implementación completa de este tutorial se puede encontrar en el proyecto GitHub; este es un proyecto basado en Maven, por lo que debería ser fácil de importar y ejecutar tal como está.