#axum #csrf #security #middleware

axum-csrf-sync-pattern

An axum layer and middleware, providing Cross Site Request Forgery protection by implementing the CSRF Synchronizer Token Pattern for same-site and cross-site API requests, as described by OWASP

7 releases

Uses new Rust 2021

0.2.2 Dec 1, 2022
0.2.0 Nov 29, 2022
0.1.4 Nov 29, 2022

#93 in HTTP server

Download history 135/week @ 2022-11-23 59/week @ 2022-11-30

194 downloads per month

MPL-2.0 license

47KB
481 lines

Axum Synchronizer Token Pattern CSRF prevention

This crate provides a Cross-Site Request Forgery protection layer and middleware for use with the axum web framework.

Crates.io Documentation

The middleware implements the CSRF Synchronizer Token Pattern for AJAX backends and API endpoints as described in the OWASP CSRF prevention cheat sheet.

More information about this crate can be found in the crate documentation.

Installation

axum-csrf-sync-pattern = "0.2.0"

Examples

See the example projects for same-site and cross-site usage.

Consider as well to use the crate unit tests as your reference.

Scope

This middleware implements token transfer via custom request headers.

The middleware requires and is built upon axum_sessions, which in turn uses async_session.

The current version is built for and works with axum 0.6.x, axum-sessions 0.4.x and async_session 3.x.

There will be support for axum 0.7 and later versions.

The Same Origin Policy prevents the custom request header to be set by foreign scripts.

In which contexts should I use this middleware?

The goal of this middleware is to prevent cross-site request forgery attacks specifically in applications communicating with their backend by means of the JavaScript fetch() API or classic XmlHttpRequest, traditionally called "AJAX".

The Synchronizer Token Pattern is especially useful in CORS contexts, as the underlying session cookie is obligatorily secured and inaccessible by JavaScript, while the custom HTTP response header carrying the CSRF token can be exposed using the CORS Access-Control-Expose-Headers HTTP response header.

While the Same Origin Policy commonly prevents custom request headers to be set on cross-origin requests, use of the use of the Access-Control-Allow-Headers CORS HTTP response header can be used to specifically allow CORS requests to be equipped with a required custom HTTP request header.

This approach ensures that requests forged by auto-submitted forms or other data-submitting scripts from foreign origins are unable to add the required header.

When should I use other CSRF protection patterns or libraries?

Use other available middleware libraries if you plan on submitting classical HTML forms without the use of JavaScript, and if you do not send the form data across origins.

Security

Token randomness

The CSRF tokens are generated using rand::ThreadRng which is considered cryptographically secure (CSPRNG). See "Our RNGs" for more.

Underlying session security

The security of the underlying session is paramount - the CSRF prevention methods applied can only be as secure as the session carrying the server-side token.

  • When creating your SessionLayer, make sure to use at least 64 bytes of cryptographically secure randomness.
  • Do not lower the secure defaults: Keep the session cookie's secure flag on.
  • Use the strictest possible same-site policy.

CORS security

If you need to provide and secure cross-site requests:

  • Allow only your backend origin when configuring the CorsLayer
  • Allow only the headers you need. (At least the CSRF request token header.)
  • Only expose the headers you need. (At least the CSRF response token header.)

No leaks of error details

Errors are logged using [tracing::error!]. Error responses do not contain error details.

Use tower_http::TraceLayer to capture and view traces.

Safety

This crate uses no unsafe code.

The layer and middleware functionality is tested. View the module source code to learn more.

Usage

See the example projects for same-site and cross-site usage. These examples are interactive demos. Run them, then interact with them in the browser.

Same-site usage

Note: The crate repository contains example projects for same-site and cross-site usage! In each example directory, execute cargo run, then open http://127.0.0.1:3000 in your browser.

Configure your session and CSRF protection layer in your backend application:

use axum::{
    body::Body,
    http::StatusCode,
    routing::{get, Router},
};
use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken};
use axum_sessions::{async_session::MemoryStore, SessionLayer};
use rand::RngCore;

let mut secret = [0; 64];
rand::thread_rng().try_fill_bytes(&mut secret).unwrap();

async fn handler() -> StatusCode {
    StatusCode::OK
}

let app = Router::new()
 .route("/", get(handler).post(handler))
 .layer(
     CsrfLayer::new()

     // Optionally, configure the layer with the following options:

     // Default: RegenerateToken::PerSession
     .regenerate(RegenerateToken::PerUse)
     // Default: "X-CSRF-TOKEN"
     .request_header("X-Custom-Request-Header")
     // Default: "X-CSRF-TOKEN"
     .response_header("X-Custom-Response-Header")
     // Default: "_csrf_token"
     .session_key("_custom_session_key")
 )
 .layer(SessionLayer::new(MemoryStore::new(), &secret));

// Use hyper to run `app` as service and expose on a local port or socket.

use tower::util::ServiceExt;
tokio_test::block_on(async {
    app.oneshot(
        axum::http::Request::builder().body(axum::body::Body::empty()).unwrap()
    ).await.unwrap();
})

Receive the token and send same-site requests, using your custom header:

const test = async () => {
  // Receive CSRF token (Default response header name: 'X-CSRF-TOKEN')
  const token = (await fetch("/")).headers.get("X-Custom-Response-Header");

  // Submit data using the token
  await fetch("/", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // Default request header name: 'X-CSRF-TOKEN'
      "X-Custom-Request-Header": token,
    },
    body: JSON.stringify({
      /* ... */
    }),
  });
};

For a full demo, run the same-site example project. You will find the interactive demo at http://127.0.0.1:3000.

CORS-enabled usage

Note: The crate repository contains example projects for same-site and cross-site usage! In each example directory, execute cargo run, then open http://127.0.0.1:3000 in your browser.

Configure your CORS layer, session and CSRF protection layer in your backend application:

use axum::{
    body::Body,
    http::{header, Method, StatusCode},
    routing::{get, Router},
};
use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken};
use axum_sessions::{async_session::MemoryStore, SessionLayer};
use rand::RngCore;
use tower_http::cors::{AllowOrigin, CorsLayer};

let mut secret = [0; 64];
rand::thread_rng().try_fill_bytes(&mut secret).unwrap();

async fn handler() -> StatusCode {
    StatusCode::OK
}

let app = Router::new()
 .route("/", get(handler).post(handler))
 .layer(
     // See example above for custom layer configuration.
     CsrfLayer::new()
 )
 .layer(SessionLayer::new(MemoryStore::new(), &secret))
 .layer(
     CorsLayer::new()
         .allow_origin(AllowOrigin::list(["https://www.example.com".parse().unwrap()]))
         .allow_methods([Method::GET, Method::POST])
         .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()])
         .allow_credentials(true)
         .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]),
);

// Use hyper to run `app` as service and expose on a local port or socket.

use tower::util::ServiceExt;
tokio_test::block_on(async {
    app.oneshot(
        axum::http::Request::builder().body(axum::body::Body::empty()).unwrap()
    ).await.unwrap();
})

Receive the token and send cross-site requests, using your custom header:

const test = async () => {
  // Receive CSRF token
  const token = (
    await fetch("https://backend.example.com/", {
      credentials: "include",
    })
  ).headers.get("X-CSRF-TOKEN");

  // Submit data using the token
  await fetch("https://backend.example.com/", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-TOKEN": token,
    },
    credentials: "include",
    body: JSON.stringify({
      /* ... */
    }),
  });
};

For a full demo, run the cross-site example project. You will find the interactive demo at http://127.0.0.1:3000.

Contributing

Pull requests are welcome!

Dependencies

~11–18MB
~322K SLoC