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 add a CSRF token to a link or form#
To pass a CSRF token you need either to:
pass an HTTP GET parameter name
_authenticator
with the token as the value,include a form field named
_authenticator
with the token as the value and submit it with the form, oradd an HTTP header named
X-CSRF-TOKEN
with the token as the value.
To add a token as an HTTP GET parameter to a link in a template, you can utilize the authenticator view:
<tal:authenticator tal:define="token context/@@authenticator/token">
<a href="${python:context.absolute_url()}/myprotected_view?_authenticator=${token}" >Link to some view</a>
</tal:authenticator>
To add a hidden field with a token to a form in a template, the above view can be used as follows:
<span tal:replace="structure context/@@authenticator/authenticator"/>
In Python code, a helper function can be used:
from plone.protect.authenticator import createToken
token = createToken()
To add an authenticator token to an existing URL with query parameters:
from plone.protect.authenticator import createToken
from urllib.parse import urlencode
from urllib.parse import urlparse
from urllib.parse import urlunparse
# The existing URL that you want to add a query parameter to
url = f"https://www.example.com?param1=value1"
# Parse the URL into its component parts
parsed_url = urlparse(url)
# Add the new query parameters to the 'query' component of the URL
token_query = urlencode({"_authenticator": createToken()})
new_query = f"{parsed_url.query}&{token_query}"
# Reassemble the URL with the updated query string
final_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
new_query,
parsed_url.fragment)
)
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:
Identify the modified object as a single object in the database.
If an attribute of the object is a "persistent" attribute (for example, a
PersistentDict
orPersistentList
instance, aBTree
, or anannotation
), use this instead.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.