|
1 | 1 | --- |
2 | 2 | myst: |
3 | 3 | html_meta: |
4 | | - "description": "" |
5 | | - "property=og:description": "" |
6 | | - "property=og:title": "" |
7 | | - "keywords": "" |
| 4 | + "description": "How to protect Plone against CSRF attacks." |
| 5 | + "property=og:description": "How to protect Plone against CSRF attacks." |
| 6 | + "property=og:title": "Cross-Site Request Forgery protection in Plone" |
| 7 | + "keywords": "CSRF, security, token, protection, Cross-Site Request Forgery" |
8 | 8 | --- |
9 | 9 |
|
10 | 10 | (classic-ui-csrf-label)= |
11 | 11 |
|
12 | 12 | # Cross-Site Request Forgery (CSRF) |
13 | 13 |
|
| 14 | +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. |
| 15 | +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. |
| 16 | +This can allow an attacker to perform actions on the web application without the user's knowledge or consent. |
| 17 | + |
| 18 | +For example, consider a web application that allows users to transfer money between accounts. |
| 19 | +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. |
| 20 | +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. |
| 21 | + |
| 22 | +To protect against CSRF attacks, Plone uses CSRF tokens to verify the authenticity of requests. |
| 23 | +CSRF tokens are unique, secret values that are generated by the web application and included in forms and links. |
| 24 | +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. |
| 25 | +If the token is missing or invalid, the request is rejected. |
| 26 | + |
| 27 | +## Auto protection |
| 28 | + |
| 29 | +In Plone, CSRF protection is done almost transparently by [`plone.protect`](https://pypi.org/project/plone.protect/). |
| 30 | +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. |
| 31 | +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. |
| 32 | + |
| 33 | +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. |
| 34 | +This includes, but is not limited to the following: |
| 35 | + |
| 36 | +- add and edit forms |
| 37 | +- control panels |
| 38 | +- custom z3c forms |
| 39 | + |
| 40 | +## Manual protection |
| 41 | + |
| 42 | +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. |
| 43 | + |
| 44 | +`plone.protect` offers the `@protect` decorator. |
| 45 | +The decorator expects a callable to perform the check. |
| 46 | +There are two checks implemented in `plone.protect`: |
| 47 | + |
| 48 | +### CSRF token check with `CheckAuthenticator` |
| 49 | + |
| 50 | +Checks whether a valid CSRF token is present in the request and raises `Unauthorized` if not. |
| 51 | + |
| 52 | +Usage example: |
| 53 | + |
| 54 | +```python |
| 55 | +from plone.protect import CheckAuthenticator |
| 56 | +from plone.protect import protect |
| 57 | + |
| 58 | +@protect(CheckAuthenticator) |
| 59 | +def write_to_api_or_service(self): |
| 60 | + # code here |
| 61 | + ... |
| 62 | +``` |
| 63 | + |
| 64 | +### HTTP POST check with `PostOnly` |
| 65 | + |
| 66 | +Checks whether the request is an HTTP POST request, and raises `Unauthorized` if not. |
| 67 | +This helps to mitigate clicks on malicious links. |
| 68 | + |
| 69 | +Usage example: |
| 70 | + |
| 71 | +```python |
| 72 | +from plone.protect import PostOnly |
| 73 | +from plone.protect import protect |
| 74 | + |
| 75 | +@protect(PostOnly) |
| 76 | +def write_to_api_or_service(self): |
| 77 | + # code here |
| 78 | + ... |
| 79 | +``` |
| 80 | + |
| 81 | +## How to add a CSRF token to a link or form |
| 82 | + |
| 83 | +To pass a CSRF token you need either to: |
| 84 | + |
| 85 | +- pass an HTTP GET parameter name `_authenticator` with the token as the value, |
| 86 | +- include a form field named `_authenticator` with the token as the value and submit it with the form, or |
| 87 | +- add an HTTP header named `X-CSRF-TOKEN` with the token as the value. |
| 88 | + |
| 89 | +To add a token as an HTTP GET parameter to a link in a template, you can utilize the authenticator view: |
| 90 | + |
| 91 | +```html |
| 92 | +<tal:authenticator tal:define="token context/@@authenticator/token"> |
| 93 | + <a href="${python:context.absolute_url()}/myprotected_view?_authenticator=${token}" >Link to some view</a> |
| 94 | +</tal:authenticator> |
| 95 | +``` |
| 96 | + |
| 97 | +To add a hidden field with a token to a form in a template, the above view can be used as follows: |
| 98 | + |
| 99 | +```html |
| 100 | +<span tal:replace="structure context/@@authenticator/authenticator"/> |
| 101 | +``` |
| 102 | + |
| 103 | +In Python code, a helper function can be used: |
| 104 | + |
| 105 | +```python |
| 106 | +from plone.protect.authenticator import createToken |
| 107 | + |
| 108 | +token = createToken() |
| 109 | +``` |
| 110 | + |
| 111 | +To add an authenticator token to an existing URL with query parameters: |
| 112 | + |
| 113 | +```python |
| 114 | +from plone.protect.authenticator import createToken |
| 115 | +from urllib.parse import urlencode |
| 116 | +from urllib.parse import urlparse |
| 117 | +from urllib.parse import urlunparse |
| 118 | + |
| 119 | +# The existing URL that you want to add a query parameter to |
| 120 | +url = f"https://www.example.com?param1=value1" |
| 121 | + |
| 122 | +# Parse the URL into its component parts |
| 123 | +parsed_url = urlparse(url) |
| 124 | + |
| 125 | +# Add the new query parameters to the 'query' component of the URL |
| 126 | +token_query = urlencode({"_authenticator": createToken()}) |
| 127 | +new_query = f"{parsed_url.query}&{token_query}" |
| 128 | + |
| 129 | +# Reassemble the URL with the updated query string |
| 130 | +final_url = urlunparse( |
| 131 | + ( |
| 132 | + parsed_url.scheme, |
| 133 | + parsed_url.netloc, |
| 134 | + parsed_url.path, |
| 135 | + parsed_url.params, |
| 136 | + new_query, |
| 137 | + parsed_url.fragment) |
| 138 | +) |
| 139 | +``` |
| 140 | + |
| 141 | + |
| 142 | +## How to allow writes in absence of a protecting token |
| 143 | + |
| 144 | +To allow certain objects to be modified and written to the database without protection, follow these steps: |
| 145 | + |
| 146 | +1. Identify the modified object as a single object in the database. |
| 147 | +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. |
| 148 | +3. Use the `safeWrite` function to mark the object as safe for writing. |
| 149 | + |
| 150 | +```{note} |
| 151 | +This is the preferred method for allowing modification and writing of specific objects to the database. |
| 152 | +``` |
| 153 | + |
| 154 | +```python |
| 155 | +from plone.protect.utils import safeWrite |
| 156 | + |
| 157 | +def some_function(obj, request): |
| 158 | + safeWrite(obj, request) |
| 159 | + obj.foo = "bar" # modify obj |
| 160 | +``` |
| 161 | + |
| 162 | +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. |
| 163 | +Then the request can be marked with the `IDisableCSRFProtection` marker interface. |
| 164 | + |
| 165 | +```python |
| 166 | +from plone.protect.interfaces import IDisableCSRFProtection |
| 167 | +from zope.interface import alsoProvides |
| 168 | + |
| 169 | +def some_function(request): |
| 170 | + alsoProvides(request, IDisableCSRFProtection) |
| 171 | + # modify the database here |
| 172 | +``` |
| 173 | + |
| 174 | +Disabling all CSRF protection for the whole Plone instance is possible by starting Plone with the environment variable `PLONE_CSRF_DISABLED=true` set. |
| 175 | +This is not recommended but can be handy temporarily in special situations. |
| 176 | + |
| 177 | + |
| 178 | +## Further reading |
| 179 | + |
| 180 | +```{seealso} |
| 181 | +The [README file of `plone.protect`](https://github.com/plone/plone.protect/blob/master/README.rst) explains the usage and also validation in detail. |
| 182 | +``` |
0 commit comments