Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

The basics

CSRF protection

Oriel protects your forms from cross-site request forgery: every form that changes something must send the session's CSRF token, or the request is refused.

Introduction#

Cross-site request forgery is an attack in which another site makes your users' browsers send requests to your app. Picture a /user/email route that accepts a POST and changes the signed-in user's email address. Without CSRF protection, any site the user visits could hold a form that posts its own address to that route, and submit it with a little JavaScript. The browser would send the user's session cookie along, and your app would change the address.

To stop this, your app checks every POST, PUT, PATCH and DELETE request to a route in the web group for a secret value, the CSRF token, which only your own pages know.

Preventing CSRF requests#

Every session has a CSRF token. It is kept in the session, so another site can't read it, and it changes whenever the session is regenerated, as the starter kits do when a user signs in. Read it from the request's session, or with csrfToken():

Route.get('/token', () => {  const token = request().session().token()  return csrfToken()})

A form you write yourself sends the token in a hidden _token field, which csrfField() writes:

resources/views/profile.tsx
import { csrfField } from '@orieljs/framework'export default function Profile() {  return (    <form method="post" action="/profile">      {csrfField()}      {/* the same as <input type="hidden" name="_token" value={csrfToken()} autocomplete="off" /> */}      <button type="submit">Save</button>    </form>  )}

The VerifyCsrfToken middleware, in the web middleware group, checks that the token a request sends matches its session's. GET, HEAD and OPTIONS requests pass without one. A request whose token is missing or wrong gets a 419: the "Page Expired" page, or, when the request wants JSON, {"message": "CSRF token mismatch."}.

Inertia pages and Live components send the token for you, from the XSRF-TOKEN cookie (see X-XSRF-TOKEN), so the starter kits' forms have no _token field.

A form that should reach a PUT, PATCH or DELETE route posts with a hidden _method field as well, from methodField(): see form method spoofing.

CSRF tokens and SPAs#

A single-page app served from your app's own domain sends the token in the X-XSRF-TOKEN header, as Inertia does. A frontend on another domain, or a mobile app, uses your API routes instead, which have no session and no CSRF check, and signs in with personal access tokens. Oriel sends no CORS headers, so a browser frontend on another domain needs a middleware of your own that adds them.

Excluding URIs from CSRF protection#

Some routes can't get the token: a payment provider's webhook, for one, doesn't know it. Such routes are best kept out of the web group, in routes/api.ts. Otherwise, list their URIs in validateCsrfTokens() in bootstrap/app.ts:

Application.configure({ basePath: '.' }).withMiddleware((middleware) => {  middleware.validateCsrfTokens({    except: ['stripe/*', 'http://example.com/foo/bar', 'http://example.com/foo/*'],  })})

An entry is a path, with or without its leading /, or a full URL, and * matches anything. The same list can live in csrf_except in config/http.ts; validateCsrfTokens() adds to it. A single route can also leave the check out: Route.post('/hooks/github', handler).withoutMiddleware(VerifyCsrfToken).

X-CSRF-TOKEN#

VerifyCsrfToken also reads the token from an X-CSRF-TOKEN header. You can put the token in a meta tag in your layout:

resources/views/partials/head.tsx
import { csrfToken } from '@orieljs/framework'export default function Head({ title }: { title: string }) {  return (    <head>      <title>{title}</title>      <meta name="csrf-token" content={csrfToken()} />    </head>  )}

and send it from your scripts:

resources/js/comments.ts
export function postComment(body: string): Promise<Response> {  const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content ?? ''  return fetch('/comments', {    method: 'POST',    headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': token },    body: JSON.stringify({ body }),  })}

X-XSRF-TOKEN#

Every response that passes VerifyCsrfToken carries the token in an XSRF-TOKEN cookie, encrypted as your other cookies are. The cookie isn't HttpOnly, so your scripts can read it, and a request may send its value back in an X-XSRF-TOKEN header, which VerifyCsrfToken decrypts. It takes its lifetime, path, domain, secure and same_site settings from config/session.ts.

The cookie is there for the clients that send it on their own: Inertia's HTTP client and Live's client do, as Axios and Angular's HttpClient do.

Privacy policy ·

esc