diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md
index 8cb9f5fc..5e3639e0 100644
--- a/docs/docs/configuration.md
+++ b/docs/docs/configuration.md
@@ -11,7 +11,7 @@ Rwf will automatically load configuration settings from that file, as they are n
The configuration file is using the [TOML language](https://toml.io/). If you're not familiar with TOML, it's pretty simple and expressive language commonly used in the world of Rust programming.
-Rwf configuration file is split into multiple sections. The `[general]` section controls various options such as logging settings, and which secret key to use for [encryption](encryption.md). The `[database]`
+Rwf configuration file is split into multiple sections. The `[general]` section controls various options such as logging settings, and which secret key to use for [encryption](security/encryption.md). The `[database]`
section configures database connection settings, like the database URL, connection pool size, and others.
### `[general]`
@@ -19,8 +19,9 @@ section configures database connection settings, like the database URL, connecti
| Setting | Description | Default |
|---------|-------------|---------|
| `log_queries` | Toggles logging of all SQL queries executed by the [ORM](models/index.md). | `false` |
-| `secret_key` | Secret key, encoded using base64, used for [encryption](encryption.md). | Randomly generated |
+| `secret_key` | Secret key, encoded using base64, used for [encryption](security/encryption.md). | Randomly generated |
| `cache_templates` | Toggle caching of [dynamic templates](views/templates/index.md). | `false` in debug, `true` in release |
+| `csrf_protection` | Validate the [CSRF](security/CSRF.md) token is present on requests that mutate your application (POST, PUT, PATCH). | `true` |
#### Secret key
diff --git a/docs/docs/controllers/cookies.md b/docs/docs/controllers/cookies.md
index 35c3ae41..639ef2c5 100644
--- a/docs/docs/controllers/cookies.md
+++ b/docs/docs/controllers/cookies.md
@@ -61,7 +61,7 @@ response
.add_private(cookie)?;
```
-Cookies are [encrypted](../encryption.md) with AES-128, using the security key set in the [configuration](../configuration.md).
+Cookies are [encrypted](../security/encryption.md) with AES-128, using the security key set in the [configuration](../configuration.md).
### Read private cookies
diff --git a/docs/docs/controllers/sessions.md b/docs/docs/controllers/sessions.md
index 2efe8ce4..f13c0dc7 100644
--- a/docs/docs/controllers/sessions.md
+++ b/docs/docs/controllers/sessions.md
@@ -1,6 +1,6 @@
# Sessions
-A session is an [encrypted](../encryption.md) [cookie](cookies.md) managed by Rwf. It contains a unique identifier for each browser using your web app. All standard-compliant browsers connecting to Rwf-powered apps will have a Rwf session set automatically, and should send it back on each request.
+A session is an [encrypted](../security/encryption.md) [cookie](cookies.md) managed by Rwf. It contains a unique identifier for each browser using your web app. All standard-compliant browsers connecting to Rwf-powered apps will have a Rwf session set automatically, and should send it back on each request.
## Session types
diff --git a/docs/docs/security/CSRF.md b/docs/docs/security/CSRF.md
new file mode 100644
index 00000000..30d4c82e
--- /dev/null
+++ b/docs/docs/security/CSRF.md
@@ -0,0 +1,95 @@
+# CSRF protection
+
+Cross-site request forgery[^1] (or CSRF) is a type of attack which uses your website's forms to trick the user into submitting data to your application from somewhere else. Rwf comes with [middleware](../controllers/middleware.md) to protect your application against such attacks.
+
+## Enable CSRF protection
+
+CSRF protection is enabled by default. When users make `POST`, `PUT`, and `PATCH` requests to your app, Rwf will check for the presence of a CSRF token. If the token is not there, or has expired, the request will be blocked and `HTTP 400 - Bad Request` response will be returned.
+
+## Passing the token
+
+The CSRF token can be passed using one of two methods:
+
+- `X-CSRF-Token` HTTP header
+- `` inside a form
+
+If you're submitting a form, you can add the `rwf_csrf_token` input automatically:
+
+```html
+
+```
+
+If you're making AJAX requests (using `fetch`, for example), you can pass the token via the header. If you're using Stimulus (which comes standard with Rwf), you can pass the token via a data attribute to the Stimulus controller:
+
+=== "HTML"
+ ```html
+
+
+
+ ```
+=== "JavaScript"
+ ```javascript
+ import { Controller } from "hotwired/stimulus"
+
+ export default class LoginController extends Controller {
+
+ // Send request with CSRF token included.
+ sendRequest() {
+ const csrfToken = this.element.dataset.csrfToken;
+
+ fetch("/login", {
+ headers: {
+ "X-CSRF-Token": csrfToken,
+ }
+ })
+ }
+
+ }
+ ```
+
+## Disable CSRF protection
+
+If you want to disable CSRF protection, you can do so globally by toggling the `csrf_protection` [configuration option](../configuration.md) to `false`, or on the controller level by implementing the `fn skip_csrf(&self)` method:
+
+```rust
+use rwf::prelude::*;
+
+#[derive(Default)]
+struct IndexController;
+
+impl Controller for IndexController {
+ /// Disable CSRF protection for this controller.
+ fn skip_csrf(&self) -> bool {
+ true
+ }
+
+ /* ... */
+}
+```
+
+### REST
+
+If you're using JavaScript frameworks like React or Vue for your frontend, it's common to disable CSRF protection on your [REST](../controllers/REST/index.md) controllers. To do so, you can add the `#[skip_csrf]` attribute to your `ModelController`, for example:
+
+```rust
+#[derive(macros::ModelController)]
+#[skip_csrf]
+struct Users;
+```
+
+You can always disable CSRF globally via [configuration](#disable-csrf-protection) and enable it only on the controllers that serve HTML forms.
+
+## Token validity
+
+The CSRF token is valid for the same duration as Rwf [sessions](../controllers/sessions.md). By default, this is set to 4 weeks. A new token is generated every time your users load a page which contains a token generated with the built-in template functions.
+
+## WSGI / Rack controllers
+
+Rwf CSRF protection is disabled for [Python](../migrating-from-python.md) and [Rails](../migrating-from-rails.md) applications. It's expected that Django/Flask/Rails applications will use their own CSRF protection middleware.
+
+[^1]: [https://en.wikipedia.org/wiki/Cross-site_request_forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery)
diff --git a/docs/docs/encryption.md b/docs/docs/security/encryption.md
similarity index 81%
rename from docs/docs/encryption.md
rename to docs/docs/security/encryption.md
index 3f1b1232..7163d0a6 100644
--- a/docs/docs/encryption.md
+++ b/docs/docs/security/encryption.md
@@ -1,39 +1,39 @@
-# Encryption
-
-Rwf uses [AES-128](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) for encrypting user [sessions](controllers/sessions.md) and private [cookies](controllers/cookies.md). The same functionality is available through the [`rwf::crypto`](https://docs.rs/rwf/latest/rwf/crypto/index.html) module to encrypt and decrypt arbitrary data.
-
-## Encrypt data
-
-To encrypt data using AES-128 and the application secret key, you can use the [`encrypt`](https://docs.rs/rwf/latest/rwf/crypto/fn.encrypt.html) function, for example:
-
-```rust
-use rwf::crypto::encrypt;
-
-let data = serde_json::json!({
- "user": "test",
- "password": "hunter2"
-});
-
-// JSON is converted into a byte array.
-let data = serde_json::to_vec(&data).unwrap();
-
-// Data is encrypted with AES.
-let encrypted = encrypt(&data).unwrap();
-```
-
-Any kind of data can be encrypted, as long as it's serializable to an array of bytes. Serialization can typically be achieved by using [`serde`](https://docs.rs/serde/latest/serde/).
-
-Encryption produces a base64-encoded UTF-8 string. You can save this string in the database or send it via an insecure medium like email.
-
-## Decrypt data
-
-To decrypt the data, you can call the [`decrypt`](https://docs.rs/rwf/latest/rwf/crypto/fn.decrypt.html) function on the string produced by the `encrypt` function. The decryption algorithm will automatically convert the base64-encoded string to bytes and decrypt those bytes using the secret key, for example:
-
-```rust
-use rwf::crypto::decrypt;
-
-let decrypted = decrypt(&encrypted).unwrap();
-let json = serde_json::from_slice(&decrypted).unwrap();
-
-assert_eq!(json["user"], "test");
-```
+# Encryption
+
+Rwf uses [AES-128](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) for encrypting user [sessions](../controllers/sessions.md) and private [cookies](../controllers/cookies.md). The same functionality is available through the [`rwf::crypto`](https://docs.rs/rwf/latest/rwf/crypto/index.html) module to encrypt and decrypt arbitrary data.
+
+## Encrypt data
+
+To encrypt data using AES-128 and the application secret key, you can use the [`encrypt`](https://docs.rs/rwf/latest/rwf/crypto/fn.encrypt.html) function, for example:
+
+```rust
+use rwf::crypto::encrypt;
+
+let data = serde_json::json!({
+ "user": "test",
+ "password": "hunter2"
+});
+
+// JSON is converted into a byte array.
+let data = serde_json::to_vec(&data).unwrap();
+
+// Data is encrypted with AES.
+let encrypted = encrypt(&data).unwrap();
+```
+
+Any kind of data can be encrypted, as long as it's serializable to an array of bytes. Serialization can typically be achieved by using [`serde`](https://docs.rs/serde/latest/serde/).
+
+Encryption produces a base64-encoded UTF-8 string. You can save this string in the database or send it via an insecure medium like email.
+
+## Decrypt data
+
+To decrypt the data, you can call the [`decrypt`](https://docs.rs/rwf/latest/rwf/crypto/fn.decrypt.html) function on the string produced by the `encrypt` function. The decryption algorithm will automatically convert the base64-encoded string to bytes and decrypt those bytes using the secret key, for example:
+
+```rust
+use rwf::crypto::decrypt;
+
+let decrypted = decrypt(&encrypted).unwrap();
+let json = serde_json::from_slice(&decrypted).unwrap();
+
+assert_eq!(json["user"], "test");
+```