What CSRF is and when it matters

CSRF exploits the browser’s tendency to automatically attach cookies to requests. If your app uses session cookies for login, a third-party site may be able to cause the victim’s browser to submit a request such as:

  • changing an email address
  • updating a password
  • transferring funds
  • creating an API token
  • deleting a resource

The attacker does not need access to the victim’s session cookie. They only need the browser to send it.

CSRF is most relevant when all of the following are true:

  • authentication is cookie-based
  • the request performs a state change
  • the browser can initiate the request cross-site
  • the server does not verify that the request came from your own site

If your API uses bearer tokens in an Authorization header and never relies on cookies, CSRF risk is much lower. But for traditional web apps and many hybrid systems, CSRF defenses are essential.

The core defense strategy

A strong CSRF defense usually combines several layers:

DefensePurposeNotes
CSRF tokenProves the request originated from your appMost common and reliable defense
SameSite cookiesReduces cross-site cookie sendingHelpful, but not sufficient alone
Origin/Referer checksVerifies request sourceUseful as an additional signal
Re-authentication for sensitive actionsLimits damage if a token is stolenBest for high-risk operations

The most important mechanism is the CSRF token. The server generates a secret token, stores it in the user’s session or a signed cookie, and requires the client to send it back with every state-changing request.

A practical token-based pattern

A common pattern is:

  1. Generate a random token when the user session starts.
  2. Embed the token in HTML forms or expose it to frontend code.
  3. Require the token in a custom header or hidden form field.
  4. Validate the token on the server before processing the request.

Because browsers do not allow arbitrary cross-site pages to read your app’s HTML or set custom headers on form submissions, the attacker cannot easily supply the correct token.

Example: validating a CSRF token in Rust

The following example shows a simple server-side validation flow. It uses a session-stored token and checks it against a token submitted in a form field or header.

use axum::{
    extract::{Form, State},
    http::{HeaderMap, StatusCode},
    response::IntoResponse,
    routing::post,
    Router,
};
use serde::Deserialize;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    // In a real app, this would be tied to the authenticated session.
    csrf_token: Arc<String>,
}

#[derive(Deserialize)]
struct UpdateProfileForm {
    display_name: String,
    csrf_token: String,
}

async fn update_profile(
    State(state): State<AppState>,
    headers: HeaderMap,
    Form(form): Form<UpdateProfileForm>,
) -> impl IntoResponse {
    let header_token = headers
        .get("x-csrf-token")
        .and_then(|v| v.to_str().ok());

    let token_matches = header_token
        .map(|t| t == state.csrf_token.as_str())
        .unwrap_or(false)
        || form.csrf_token == *state.csrf_token;

    if !token_matches {
        return (StatusCode::FORBIDDEN, "invalid csrf token");
    }

    // Perform the state-changing action here.
    let _name = form.display_name;

    (StatusCode::OK, "profile updated")
}

fn app(state: AppState) -> Router {
    Router::new()
        .route("/profile", post(update_profile))
        .with_state(state)
}

This example is intentionally simple. In production, the token should be tied to the authenticated session, not stored globally. The key idea is that the server must compare a client-supplied token with a server-known value before allowing the action.

How to generate tokens safely

CSRF tokens must be unpredictable. Use a cryptographically secure random number generator and sufficient entropy. A token should be long enough that guessing it is infeasible.

A good practice is to generate a fresh token per session, and optionally rotate it after login or privilege changes. For especially sensitive workflows, you can also issue per-request or per-form tokens.

Token generation checklist

  • use a secure RNG
  • encode the token safely for HTML or headers
  • bind the token to a specific session
  • invalidate it on logout
  • rotate it after authentication changes

Do not reuse a token across unrelated users or environments. Never derive it from user IDs, timestamps, or other predictable values.

Where to place the token

There are two common approaches:

  1. Hidden form field for traditional HTML forms
  2. Custom request header for JavaScript-driven clients

Hidden form field

This is ideal for server-rendered applications. The server includes the token in the form, and the browser submits it back with the POST request.

Example:

<form method="post" action="/profile">
  <input type="hidden" name="csrf_token" value="{{csrf_token}}">
  <input type="text" name="display_name">
  <button type="submit">Save</button>
</form>

Custom header

This is useful for single-page apps or fetch-based clients. The frontend reads the token from a safe source, such as a meta tag or bootstrap JSON, then sends it in a header like X-CSRF-Token.

Because cross-site HTML forms cannot set arbitrary headers, this raises the bar significantly for attackers.

Why SameSite cookies help, but do not replace tokens

SameSite is a valuable browser-level mitigation. It tells the browser when to include cookies in cross-site requests.

Cookie settingBehaviorSecurity impact
StrictCookies are not sent on cross-site navigationsStrongest protection, but can break login flows
LaxCookies are sent on top-level GET navigations, but not most POSTsGood default for many apps
NoneCookies are sent in all contexts if Secure is setRequired for some cross-site integrations, but highest CSRF risk

SameSite=Lax reduces many CSRF scenarios, but it is not a complete defense. Some flows still allow cross-site actions, and browser behavior can vary across edge cases. Use SameSite as a defense-in-depth measure, not as your only control.

Validate the request source as an extra signal

For browser-based requests, checking the Origin header can provide useful assurance. For same-site requests, the browser typically sends an Origin value matching your application. If the header is missing or mismatched on a state-changing request, you can reject it.

The Referer header can also help, though it is less reliable because privacy settings and intermediaries may strip it.

A practical policy is:

  • require a valid CSRF token
  • require Origin to match your site for browser POST/PUT/PATCH/DELETE requests
  • allow carefully reviewed exceptions only where necessary

Do not rely on Origin alone. Some requests may not include it, and some non-browser clients can spoof headers. It is best used as a supplemental check.

Protecting JSON endpoints

Developers sometimes assume JSON endpoints are immune to CSRF. That is not automatically true.

If your endpoint accepts cookies and processes state changes, it may still be vulnerable. The browser may not let a cross-site form send application/json directly, but attackers can sometimes exploit alternative content types or legacy behaviors. If the endpoint is authenticated with cookies, treat it as CSRF-sensitive.

For JSON APIs used by browsers:

  • require a CSRF token in a custom header
  • reject requests without the expected Content-Type
  • verify Origin
  • avoid accepting state changes via GET

If your API is truly token-authenticated with bearer tokens in headers and not cookies, CSRF is usually not the primary concern. In that case, focus more on token storage and XSS prevention.

Common mistakes to avoid

1. Using GET for state changes

GET requests should be safe and idempotent. Never use GET to delete data, change settings, or trigger payments. If a browser can load a URL, it can be abused.

2. Trusting cookies alone

Authentication cookies identify the user, but they do not prove intent. CSRF attacks specifically abuse that gap.

3. Reusing a static token

A hardcoded or application-wide token is effectively public. Tokens must be per-session and unpredictable.

4. Forgetting logout and rotation

If a session ends, its CSRF token should become invalid too. Also rotate tokens after login or privilege escalation.

5. Accepting missing tokens silently

Fail closed. If the token is absent, malformed, or mismatched, return 403 Forbidden.

6. Exposing tokens in unsafe places

Avoid putting tokens in URLs. Query strings leak through logs, browser history, and referrer headers. Prefer hidden fields or headers.

A recommended implementation strategy in Rust

For most Rust web applications, the safest practical design is:

  1. store the CSRF token in the server-side session
  2. render it into forms or expose it to frontend code
  3. require it on every state-changing request
  4. set session cookies with HttpOnly, Secure, and SameSite=Lax or Strict
  5. validate Origin for browser requests
  6. rotate the token when the session changes

This approach works well with frameworks such as Axum, Actix Web, and Rocket. The exact API differs, but the security model is the same.

Operational guidance

  • keep token validation centralized in middleware or extractors
  • log failures without echoing the token
  • test that protected endpoints reject missing and invalid tokens
  • review any endpoint that mutates state, even if it looks “internal”
  • document which routes require CSRF protection

Testing your CSRF defenses

Security controls should be tested like any other behavior. Add integration tests that confirm:

  • requests without a token return 403
  • requests with an invalid token return 403
  • requests with a valid token succeed
  • Origin mismatches are rejected if you enforce origin checks
  • logout invalidates the previous token

You can also test from a browser context to ensure your frontend sends the token correctly. This is especially important for SPAs, where token extraction and header injection are easy to misconfigure.

Summary

CSRF protection in Rust is not about a single library or framework feature. It is about designing request flows so the server can distinguish legitimate user actions from cross-site forgeries.

The most reliable pattern is to combine:

  • unpredictable per-session tokens
  • SameSite cookies
  • origin validation
  • safe HTTP method usage
  • careful handling of browser-authenticated endpoints

When you apply these controls consistently, you significantly reduce the risk of unauthorized state changes in your Rust web application.

Learn more with useful resources