Overview

This plugin provides ways of protecting your forms. Currently, it can protect them from CSRF attacks and from unwanted Double Submissions.

CSRF protection

Introduction

The CSRF protection is achieved by combining two technics:

  • Validating the origin and referer headers.
  • Validating a special protection id that is added to the form before it is submitted.
Our implementation is an adaptation of OWASP suggestions.

Filter

The first thing to do to enable CSRF protection in your application is to add the provided SpincastFormsCsrfProtectionFilter "before" filter.

Here's how we suggest you add the filter :

router.ALL()
      .pos(-100)
      .found()
      .skipResourcesRequests()
      .handle(spincastFormsCsrfProtectionFilter::handle);

Explanation :

  • 1 : The filter will be called for every kind of HTTP method. It is going to manage by itself what kind of method can be dangerous or not.
  • 2 : It needs to be a before filter, but you can adjust the exact position by taking into account your other filters.
  • 3 : Only the regular routes ("found") should be protected. You don't want to run the filter again if an exception occurs, for example.
  • 4 : There is no need to protect resources.
  • 5 : The SpincastFormsCsrfProtectionFilter filter's handler.

Adding CSRF protection ids to forms

If you use the default templating engine, Pebble, two functions are provided by the plugin to easily add CSRF protection ids to your forms:

  • formCsrfProtectionFieldName() : Outputs the name of the field to use to store the CSRF protection ids. In general, you want to use it on a hidden field.
  • formCsrfProtectionFieldValue(): Outputs the CSRF protection id to use.
For example:

<form>
    <input type="hidden" 
           name="{{ formCsrfProtectionFieldName() }}" 
           value="{{ formCsrfProtectionFieldValue() }}" />
   
    // other fields...
</form>

If you are not using Pebble, you have to generate the same field using those:

That's it, your forms are now protected against CSRF attacks!

Note that once the CSRF filter is added, all forms must send a CSRF protection id, otherwise they will be rejected!

Configuration

Have a look at the Configurations section to learn the available options to tweak this CSRF protection.

Double Submit protection

Introduction

The Double Submit protection works that way: once a form is submitted with a protection id, this id is saved on the server. If the same form (id) is submitted again, a filter will reject it.

The form will also be rejected if it is too old. The lifespan of a form is configurable.

Implementing the repository

The Double Submit protection is agnostic on how the protection ids of the submitted forms are stored on the server. This is why you have to provide the code for saving and retrieving the ids.

You do this by binding an implementation of the SpincastFormsDoubleSubmitProtectionRepository interface. There are three methods to implement :

  • void saveSubmittedFormProtectionId(Instant date, String protectedId)

    Called by the plugin when the protection id of a submitted form needs to be saved.

  • boolean isFormAlreadySubmitted(String protectedId)

    Called by the plugin to see if a form was already submitted.

  • void deleteOldFormsProtectionIds(int maxAgeMinutes)

    Called by the plugin to delete the protection ids that have been saved for more than "maxAgeMinutes" minutes.

When your implementation is ready, you bind it in your application's Guice module:

bind(SpincastFormsDoubleSubmitProtectionRepository.class)
        .to(YourAppFormsDoubleSubmitProtectionRepository.class)
        .in(Scopes.SINGLETON);

Finally, here's an example of an SQL script (targeting PostgreSQL) to create a "forms_submitted" table. You will probably have to adjust this query for your own database! Or even to use something totally different if you are not using a relational database.

CREATE TABLE forms_submitted (
    id VARCHAR(36) PRIMARY KEY,
    creation_date TIMESTAMPTZ NOT NULL
)

Filter

You also have to add a provided SpincastFormsDoubleSubmitProtectionFilter "before" filter to your router.

Here's how we suggest you add the filter:

router.ALL()
      .pos(-200)
      .found()
      .skipResourcesRequests()
      .handle(spincastFormsDoubleSubmitProtectionFilter::handle);

Explanation :

  • 1 : The filter should be called for every kind of HTTP method.
  • 2 : It needs to be a before filter, but you can adjust the exact position by taking into account your other filters.
  • 3 : Only the regular routes ("found") should be protected. You don't need to run the filter again if an exception occurs, for example.
  • 4 : There is no need to protect resources.
  • 5 : The SpincastFormsDoubleSubmitProtectionFilter filter's handler.

Adding Double Submit protection ids to forms

If you use the default templating engine, Pebble, three functions are provided by the plugin to easily add a Double Submit protection id to a form:

  • formDoubleSubmitProtectionFieldName() : This fonction will output the name of the field to use to store the Double Submit protection id. In general, you want to use it on a hidden field.
  • formDoubleSubmitProtectionFieldValue(): This fonction will output the value of the Double Submit protection id to be used.
For example:

<form>
    <input type="hidden" 
           name="{{ formDoubleSubmitProtectionFieldName() }}" 
           value="{{ formDoubleSubmitProtectionFieldValue() }}" />
   
    // other fields...
</form>

If you are not using Pebble, you have to generate the same field using those:

And that's it, your form is protected agains Double Submissions!

Disabling the Double Submit protection

Note that, by default, once the Double Submit filter is added, all forms must send a protection id, otherwise they will be rejected!

But the Double Submit protection is not always required. In fact, it sometimes even have to be disabled! For example, you may have a form that is used to upload images via Ajax. This form may well be used more than once and it's perfectly fine!

If you use Pebble, you can easily disable a form from being protected by using this function:

  • formDoubleSubmitDisableProtectionFieldName()
In your form, you add a field (most of the time an hidden field) with the name this function outputs and set any value to it ("1" for example):

<form>
    <input type="hidden" 
           name="{{ formDoubleSubmitDisableProtectionFieldName() }}" 
           value="1" />
   
    // other fields...
</form>

When the SpincastFormsDoubleSubmitProtectionFilter filter sees that a submitted form contains this field, it won't validate the form at all.

Configuration

Have a look at the Configurations section to learn the available options to tweak this Double Submit protection.

Dependencies

This plugin depends on three plugins which are not provided by default by the spincast-default artifact:

Those dependencies will be automatically installed, but you may need to configure them! For example, the Spincast Session plugin will add a default repository that uses cookies in order to store the sessions... you may want to bind a custom one instead. It will also bind two filters, by default at positions -100 and 100. You may want to bind a custom implementation of SpincastSessionConfig if you need to modify those positions!

In other words, make sure you read the documentation of the automatically installed plugins!

Installation

As you learn in the previous sections, you need to add two "before" filters, bind an implementation for the SpincastFormsDoubleSubmitProtectionRepository interface, and optionally also bind some custom configurations.

Other than that, the plugin is installed as any other one:

1. Add this Maven artifact to your project:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-forms-protection</artifactId>
    <version>2.2.0</version>
</dependency>

2. Add an instance of the SpincastFormsProtectionPlugin plugin to your Spincast Bootstrapper:


Spincast.configure()
        .plugin(new SpincastFormsProtectionPlugin())
        // ...

Configurations

The configuration interface for this plugin is SpincastFormsProtectionConfig. To change the default configurations, you can bind an implementation of that interface, extending the default SpincastFormsProtectionConfigDefault implementation if you don't want to start from scratch.

Options:

  • String getFormCsrfProtectionIdFieldName()

    The name of the field (in general a hidden field) to use in a form for CSRF protection.

    Defaults to "spincast_csrf_id".

  • String getFormDoubleSubmitProtectionIdFieldName()

    The name of the field (in general a hidden field) to use in a form for Double Submit protection.

    Defaults to "spincast_ds_id".

  • String getFormDoubleSubmitDisableProtectionIdFieldName()

    The name of the field (in general a hidden field) to use in a form to disable Double Submit protection.

    Defaults to "spincast_ds_disabled".

  • String autoRegisterDeleteOldDoubleSubmitProtectionIdsScheduledTask()

    Should the plugin automatically register a scheduled task that will call deleteOldFormsProtectionIds() to clean old Double Submit protection ids?

    Defaults to true.

  • int getDeleteOldDoubleSubmitProtectionIdsScheduledTaskRunEveryNbrMinutes()

    If autoRegisterDeleteOldDoubleSubmitProtectionIdsScheduledTask() is true, this configuration specifies the number of minutes between two launches of the scheduled task.

    Defaults to 15.

  • int getFormDoubleSubmitFormValidForNbrMinutes()

    The number of minutes a form is considered as valid. If the form is older than the specified number of minutes, it will be rejected by the Double Submit protection filter.

    Defaults to 120 (2 hours).