Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CSRF protection #21

Merged
merged 3 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ 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]`

| 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

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/controllers/cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/controllers/sessions.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
95 changes: 95 additions & 0 deletions docs/docs/security/CSRF.md
Original file line number Diff line number Diff line change
@@ -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
- `<input name="rwf_csrf_token" type="hidden">` inside a form

If you're submitting a form, you can add the `rwf_csrf_token` input automatically:

```html
<form method="post" action="/login">
<%= rwf_token() %>
</form>
```

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
<div
data-controller="login"
data-csrf-token="<%= rwf_token_raw() %>"
>
<!-- ... -->
</div>
```
=== "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)
78 changes: 39 additions & 39 deletions docs/docs/encryption.md → docs/docs/security/encryption.md
Original file line number Diff line number Diff line change
@@ -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");
```
21 changes: 20 additions & 1 deletion docs/docs/views/templates/functions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ Inserts JavaScript code which will create and initialize a [Turbo Stream](../../
[^1]: [https://turbo.hotwired.dev/handbook/streams](https://turbo.hotwired.dev/handbook/streams)



### `render`

Renders a template directly inside the current template. Can be used for rendering [partials](../partials.md). `<%%` is a special template code tag which is an alias for `render`.
Expand All @@ -122,3 +121,23 @@ Renders a template directly inside the current template. Can be used for renderi
<%% "templates/profile.html" %>
</div>
```

### `csrf_token`

Renders an input field with a valid [CSRF](../../../security/CSRF.md) token.

```html
<form action="/login" method="post">
<%= csrf_token() %>
</form>
```


### `csrf_token_raw`

Renders a valid [CSRF](../../../security/CSRF.md) token as a raw HTML string. It can then be passed to JavaScript via a `data-` attribute or a global variable:

```html
<div data-csrf-token="<%= csrf_token_raw() %>"
</div>
```
2 changes: 0 additions & 2 deletions examples/django/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions examples/quick-start/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl PageController for SignupController {
<html>
<body>
<form action="/signup" method="post">
<%= csrf_token() %>
<label>Email</labelL
<input name="email" type="email" required>
<button type="submit">Sign up</button>
Expand Down Expand Up @@ -107,10 +108,10 @@ struct SignupForm {
let form = request.form::<SignupForm>()?;
```

This will automatically extract the `email`, `password` and `password2` fields from the request FormData and map them to the struct. If any of the fields are missing or are of the wrong type, a `400 - Bad Request` will be returned automatically.
This will automatically extract the `email`, `password` and `password2` fields from the request FormData and map them to the struct. If any of the fields are missing or are of the wrong type, a `400 - Bad Request` will be returned automatically.

If you're in the HTML-over-the-wire camp, `PageController` and `Controller` will handle the vast majority of your use cases. However, if you prefer to build your frontends in JavaScript, Rwf comes with a couple more controllers that will come in handy.

## More examples

See [Rwf + Turbo](/examples/turbo) for a complete example of building a Single Page Application with Rwf, Turbo, Stimulus and WebSockets.
See [Rwf + Turbo](/examples/turbo) for a complete example of building a Single Page Application with Rwf, Turbo, Stimulus and WebSockets.
2 changes: 1 addition & 1 deletion examples/turbo/rwf.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[general]
secret_key = "TRtZ2Ww4EeY3xfA82Bo9bNCQbkLiUZmiDO6wOE0W0qw="
log_queries = true
cache_templates = true
cache_templates = false

[database]
name = "rwf_turbo"
Loading
Loading