This plugin provides ways of protecting your forms. Currently, it can protect them from CSRF attacks and from unwanted Double Submissions.
The CSRF protection is achieved by combining two technics:
origin
and referer
headers.
protection id
that is added to the form before it is submitted.
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 :
before filter
, but you can adjust
the exact position by taking into account your other filters.
found
") should be
protected. You don't want to run the filter again if an exception occurs, for example.
SpincastFormsCsrfProtectionFilter
filter's handler.
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.
<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:
SpincastFormsProtectionConfig#getFormCsrfProtectionIdFieldName()
:
to get the name of the field to use.
SpincastFormsCsrfProtectionFilter#getCurrentCsrfToken()
:
to get the protection id to use.
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!
Have a look at the Configurations section to learn the available options to tweak this CSRF protection.
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.
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 )
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 :
before filter
, but you can adjust
the exact position by taking into account your other filters.
found
") should be
protected. You don't need to run the filter again if an exception occurs, for example.
SpincastFormsDoubleSubmitProtectionFilter
filter's handler.
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.
<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:
SpincastFormsProtectionConfig#getFormDoubleSubmitProtectionIdFieldName()
:
to get the name of the field to use.
SpincastFormsDoubleSubmitProtectionFilter#createNewFormDoubleSubmitProtectionId()
:
to get the protection id to use.
And that's it, your form is protected agains Double Submissions!
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()
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.
Have a look at the Configurations section to learn the available options to tweak this Double Submit protection.
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!
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()) // ...
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.
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).