Cross-site request forgery (CSRF)#

Cross-site request forgery (CSRF or XSRF) is a type of web attack that allows an attacker to send malicious requests to a web application on behalf of a legitimate user. The attack works by tricking the user's web browser into sending a request to the web application that the user did not intentionally make. This can allow an attacker to perform actions on the web application without the user's knowledge or consent.

For example, consider a web application that allows users to transfer money between accounts. An attacker could craft a malicious link or form that, when clicked or submitted by a victim, would transfer money from the victim's account to the attacker's account. If the victim is logged into the web application and clicks the link or form, the web application would receive a request to transfer the money, and it would comply with the request because it appears to come from a legitimate user.

To protect against CSRF attacks, Plone uses CSRF tokens to verify the authenticity of requests. CSRF tokens are unique, secret values that are generated by the web application and included in forms and links. When a form or link with a valid CSRF token is submitted, the web application can verify the authenticity of the request by checking the token. If the token is missing or invalid, the request is rejected.

Auto protection#

In Plone, CSRF protection is done almost transparently by plone.protect. One important aspect of plone.protect is that it performs the CSRF token validation at the database transaction commit time (at the end of the request), rather than at the beginning of the request. This means that the view can execute and make changes to the database, but the changes will not be persisted unless a valid CSRF token is present in the request.

When a logged-in user requests a page, Plone automatically includes the CSRF token in all forms by applying a transform (using plone.transformchain) that adds a hidden input with its value set to the token. This includes, but is not limited to the following:

  • add and edit forms

  • control panels

  • custom z3c forms

Manual protection#

To ensure that code that is not part of a database transaction—such as code that writes to an external API or a service that is not automatically included in the transaction mechanism—is protected, you will need to manually implement protection for that code.

plone.protect offers the @protect decorator. The decorator expects a callable to perform the check. There are two checks implemented in plone.protect:

CSRF token check with CheckAuthenticator#

Checks whether a valid CSRF token is present in the request and raises Unauthorized if not.

Usage example:

from plone.protect import CheckAuthenticator
from plone.protect import protect

@protect(CheckAuthenticator)
def write_to_api_or_service(self):
    # code here
    ...

HTTP POST check with PostOnly#

Checks whether the request is an HTTP POST request, and raises Unauthorized if not. This helps to mitigate clicks on malicious links.

Usage example:

from plone.protect import PostOnly
from plone.protect import protect

@protect(PostOnly)
def write_to_api_or_service(self):
    # code here
    ...

How to allow writes in absence of a protecting token#

To allow certain objects to be modified and written to the database without protection, follow these steps:

  1. Identify the modified object as a single object in the database.

  2. If an attribute of the object is a "persistent" attribute (for example, a PersistentDict or PersistentList instance, a BTree, or an annotation), use this instead.

  3. Use the safeWrite function to mark the object as safe for writing.

Note

This is the preferred method for allowing modification and writing of specific objects to the database.

from plone.protect.utils import safeWrite

def some_function(obj, request):
    safeWrite(obj, request)
    obj.foo = "bar"  # modify obj

If there are lots of modifications or it is not possible to identify the boundaries of the writes, the protection can be disabled for the whole current request. Then the request can be marked with the IDisableCSRFProtection marker interface.

from plone.protect.interfaces import IDisableCSRFProtection
from zope.interface import alsoProvides

def some_function(request):
    alsoProvides(request, IDisableCSRFProtection)
    # modify the database here

Disabling all CSRF protection for the whole Plone instance is possible by starting Plone with the environment variable PLONE_CSRF_DISABLED=true set. This is not recommended but can be handy temporarily in special situations.

Further reading#

See also

The README file of plone.protect explains the usage and also validation in detail.