refrakt/csrf

Cross-site request forgery (CSRF) protection.

A CSRF attack is a page on another site submitting a form to your app, riding on the visitor’s session cookie. protect stops it by asking the browser where an unsafe request (POST, PUT, PATCH, DELETE) came from, and answering 403 Forbidden when it came from another site:

  1. Sec-Fetch-Site, sent by all current browsers over HTTPS and to localhost. same-origin and none (typed or bookmarked by the user) pass; same-site and cross-site are rejected.
  2. Otherwise Origin, compared with the Host header.
  3. Otherwise Referer, compared with the Host header.
  4. A request with none of these did not come from a browser page (curl, another server, a mobile app) or came from a very old browser. It is let through with its cookie header removed, so it cannot act as the signed-in user.

Browsers set these headers themselves and pages cannot forge them, so no token has to be threaded through your forms. This is the approach of Go’s http.CrossOriginProtection and Wisp’s csrf_known_header_protection, with Sec-Fetch-Site support and a 403 status. Keep GET and HEAD handlers free of side effects: they are never checked.

Behind a reverse proxy, forward the original Host header (nginx: proxy_set_header Host $host;). Browsers on plain HTTP do not send Sec-Fetch-Site, and the Origin check needs the host the browser saw.

Example

fn middleware(req: Request, next: fn(Request) -> Response) -> Response {
  let req = wisp.method_override(req)
  use <- wisp.log_request(req)
  use req <- csrf.protect(req)
  next(req)
}

Types

What check decided about a request.

pub type Verdict {
  Allowed
  AllowedWithoutCookies
  Rejected(reason: String)
}

Constructors

  • Allowed

    A safe method, or an unsafe one sent by a page on this site.

  • AllowedWithoutCookies

    An unsafe request without any browser provenance headers. It may go ahead, but without cookies.

  • Rejected(reason: String)

    An unsafe request sent by a page on another site.

Values

pub fn check(req: request.Request(body)) -> Verdict

Decide whether a request may go ahead. protect acts on this verdict; use check directly to build your own response.

pub fn protect(
  req: request.Request(body),
  next: fn(request.Request(body)) -> response.Response(wisp.Body),
) -> response.Response(wisp.Body)

Reject cross-site unsafe requests with 403 Forbidden, and strip the cookies from unsafe requests that carry no provenance headers.

✨ Search Document