Files
bun-src/docs/runtime/csrf.mdx
T
2026-08-27 21:09:14 +00:00

203 lines
9.0 KiB
Plaintext

---
title: CSRF Protection
description: Generate and verify CSRF tokens with Bun's built-in API
---
`Bun.CSRF` generates and verifies [CSRF (Cross-Site Request Forgery)](https://owasp.org/www-community/attacks/csrf) tokens. Bun signs tokens with HMAC. Each token includes an issue timestamp and an expiry duration.
```ts title="csrf.ts" icon="/icons/typescript.svg"
// Generate a token bound to the requester's session
const token = Bun.CSRF.generate("my-secret", { sessionId: "user-session-id" });
// Verify it
const isValid = Bun.CSRF.verify(token, { secret: "my-secret", sessionId: "user-session-id" });
console.log(isValid); // true
```
<Callout type="warning">
Always pass a `sessionId` (the requester's session identifier or user ID) to both `generate()` and `verify()`. Without
it, a token is only bound to the secret, so any token the server has ever issued validates for every user. An attacker
can therefore obtain a token in their own session and replay it in a forged cross-site request from a victim's
browser.
</Callout>
---
## `Bun.CSRF.generate()`
Generate a CSRF token. The token contains a cryptographic nonce, an issue timestamp, the `expiresIn` duration, and an HMAC signature, encoded as a string.
```ts title="generate.ts" icon="/icons/typescript.svg"
const token = Bun.CSRF.generate("my-secret-key");
```
**Parameters:**
- `secret` (string, optional) — The secret key used to sign the token. If not provided, Bun generates a random in-memory default secret (unique per thread).
- `options` (object, optional):
| Option | Type | Default | Description |
| ----------- | -------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `expiresIn` | `number` | `86400000` | Milliseconds until the token expires. Defaults to 24 hours. |
| `encoding` | `string` | `"base64url"` | Token encoding format: `"base64"`, `"base64url"`, or `"hex"`. |
| `algorithm` | `string` | `"sha256"` | HMAC algorithm: `"sha256"`, `"sha384"`, `"sha512"`, `"sha512-256"`, `"blake2b256"`, or `"blake2b512"`. |
| `sessionId` | `string` | (none) | Binds the token to the requesting principal (session ID, user ID, or equivalent). The token only verifies when you pass the same `sessionId` to `verify()`. |
**Returns:** `string` — the encoded token.
```ts title="generate-options.ts" icon="/icons/typescript.svg"
// Token bound to the requester's session that expires in 1 hour, encoded as hex
const token = Bun.CSRF.generate("my-secret", {
sessionId: "user-session-id",
expiresIn: 60 * 60 * 1000,
encoding: "hex",
});
// Using a different algorithm
const token2 = Bun.CSRF.generate("my-secret", {
sessionId: "user-session-id",
algorithm: "sha512",
});
```
---
## `Bun.CSRF.verify()`
Verify a CSRF token. Returns `true` if the token is valid and has not expired, `false` otherwise.
```ts title="verify.ts" icon="/icons/typescript.svg"
const isValid = Bun.CSRF.verify(token, { secret: "my-secret-key" });
```
**Parameters:**
- `token` (string, required) — The token to verify.
- `options` (object, optional):
| Option | Type | Default | Description |
| ----------- | -------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `secret` | `string` | (auto) | The secret used to sign the token. If not provided, uses the same in-memory default as `generate()`. |
| `maxAge` | `number` | `86400000` | Maximum token age in milliseconds, independent of the token's own `expiresIn`. |
| `encoding` | `string` | `"base64url"` | Must match the encoding used during `generate()`. |
| `algorithm` | `string` | `"sha256"` | Must match the algorithm used during `generate()`. |
| `sessionId` | `string` | (none) | Must match the `sessionId` used during `generate()`. A token bound to one principal fails verification for any other principal. A token generated without a `sessionId` fails verification when you supply one. |
**Returns:** `boolean`
```ts title="verify-options.ts" icon="/icons/typescript.svg"
// Verify a token bound to the requester's session
const isValid = Bun.CSRF.verify(token, {
secret: "my-secret",
sessionId: "user-session-id",
});
// Enforce a shorter max age than what the token was generated with
const isValid2 = Bun.CSRF.verify(token, {
secret: "my-secret",
sessionId: "user-session-id",
maxAge: 60 * 1000, // reject tokens older than 1 minute
});
```
---
## Using with `Bun.serve()`
A typical pattern is to generate a token when rendering a form, embed it in a hidden field, and verify it when the form is submitted. Pass the requester's session identifier as `sessionId` to both calls so the token only works for the user it was issued to.
```ts title="server.ts" icon="/icons/typescript.svg"
const SECRET = process.env.CSRF_SECRET || "my-secret";
// Resolve the requester's session identifier from a session cookie. Returns
// null when the visitor has no session yet — never fall back to a shared
// placeholder, or every session-less visitor would share one token binding.
function getSessionId(req: Request): string | null {
return req.headers.get("cookie")?.match(/(?:^|;\s*)session=([^;]+)/)?.[1] ?? null;
}
const server = Bun.serve({
routes: {
"/form": req => {
// Create a per-visitor session before issuing the form so the token is
// bound to this visitor and no one else.
let sessionId = getSessionId(req);
const headers = new Headers({ "Content-Type": "text/html" });
if (!sessionId) {
sessionId = crypto.randomUUID();
headers.append("Set-Cookie", `session=${sessionId}; HttpOnly; SameSite=Lax; Path=/`);
}
const token = Bun.CSRF.generate(SECRET, { sessionId });
return new Response(
`<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="${token}" />
<input type="text" name="message" />
<button type="submit">Send</button>
</form>`,
{ headers },
);
},
"/submit": {
POST: async req => {
const sessionId = getSessionId(req);
const formData = await req.formData();
const csrfToken = formData.get("_csrf");
if (!sessionId || typeof csrfToken !== "string" || !Bun.CSRF.verify(csrfToken, { secret: SECRET, sessionId })) {
return new Response("Invalid CSRF token", { status: 403 });
}
return new Response("OK");
},
},
},
});
console.log(`Listening on ${server.url}`);
```
---
## Default secret
If you omit the `secret` parameter in both `generate()` and `verify()`, Bun uses a random secret generated once per thread. The default secret is convenient for single-threaded applications, but tokens don't verify across servers or workers, or after a restart.
```ts title="default-secret.ts" icon="/icons/typescript.svg"
// Both calls use the same per-thread default secret within this runtime context.
const token = Bun.CSRF.generate();
const isValid = Bun.CSRF.verify(token); // true
```
For production use, always provide an explicit secret shared across your infrastructure.
---
## TypeScript
```ts title="types.ts" icon="/icons/typescript.svg"
type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256";
interface CSRFGenerateOptions {
expiresIn?: number;
encoding?: "base64" | "base64url" | "hex";
algorithm?: CSRFAlgorithm;
sessionId?: string;
}
interface CSRFVerifyOptions {
secret?: string;
encoding?: "base64" | "base64url" | "hex";
algorithm?: CSRFAlgorithm;
maxAge?: number;
sessionId?: string;
}
namespace Bun.CSRF {
function generate(secret?: string, options?: CSRFGenerateOptions): string;
function verify(token: string, options?: CSRFVerifyOptions): boolean;
}
```