Skip to content
File

Blob: src/worker/middleware/origin.ts

typescript35 lines
1import type { MiddlewareHandler } from "hono";
2 
3import type { AppBindings } from "@/worker/hono";
4import { HttpError } from "@/worker/http";
5 
6// CSRF boundary for tessera-owned cookie-authenticated mutating routes.
7// Better Auth runs its own originCheck inside `auth.handler` for /api/auth/*
8// requests; tessera's custom routes sit outside that and need their own
9// gate. SameSite=Lax on the session cookie blocks cross-site form POSTs,
10// but does NOT stop a sibling subdomain on the same site (e.g.
11// evil.limic.dev posting to auth.limic.dev) — Origin is the correct
12// primary signal for that case.
13//
14// Safe methods (GET/HEAD/OPTIONS) skip the check: browsers do not reliably
15// send Origin on those requests, and read-side access is gated by
16// SameSite=Lax plus the session cookie. Origin matching on safe methods
17// would block legitimate cross-tab GETs without changing the threat model.
18//
19// Order: this runs before requireUser/requireAdmin so an unauthenticated
20// foreign-origin POST short-circuits at 403 without burning a session
21// lookup.
22export const requireSameOriginForMutations: MiddlewareHandler<AppBindings> = async (c, next) => {
23 const method = c.req.method;
24 if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
25 await next();
26 return;
27 }
28 const expected = new URL(c.var.baseURL).origin;
29 const actual = c.req.header("origin");
30 if (!actual || actual !== expected) {
31 throw new HttpError(403, "forbidden_origin", "Request origin not allowed.");
32 }
33 await next();
34};