release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+32 -24
View File
@@ -1,11 +1,6 @@
/**
* CSRF protection via the double-submit cookie pattern.
*
* The framework sets a readable `wire-csrf` cookie on page loads; the client
* echoes it in an `x-csrf-token` header on unsafe requests (the Wire UI form
* runtime does this automatically). The server checks header === cookie. A
* cross-site attacker can't read the cookie to forge the header, so the request
* is rejected — while same-origin requests pass.
* CSRF protection via the double-submit cookie pattern plus origin/fetch
* metadata validation for unsafe requests.
*/
import type { Context, Middleware } from "./context.ts";
@@ -15,12 +10,20 @@ export const CSRF_HEADER = "x-csrf-token";
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
export interface CsrfProtectionOptions {
/** Validate Origin when present. Defaults to true. */
verifyOrigin?: boolean;
/** Additional exact origins permitted for trusted cross-origin clients. */
trustedOrigins?: string[];
/** Reject Sec-Fetch-Site: cross-site on unsafe requests. Defaults to true. */
verifyFetchMetadata?: boolean;
}
/** Ensure the CSRF cookie exists (readable by JS) and return its token. */
export function csrfToken(ctx: Context): string {
let token = ctx.cookies.get(CSRF_COOKIE);
if (!token) {
token = crypto.randomUUID().replace(/-/g, "");
// Readable by JS (double-submit needs it) but Secure on HTTPS.
ctx.cookies.set(CSRF_COOKIE, token, {
sameSite: "Lax",
path: "/",
@@ -30,33 +33,38 @@ export function csrfToken(ctx: Context): string {
return token;
}
/**
* Verify an unsafe request's CSRF token against the cookie. Safe methods
* (GET/HEAD/OPTIONS) always pass. The token may arrive in the `x-csrf-token`
* header or a `_csrf` field already parsed onto `ctx.locals`.
*/
export function verifyCsrf(ctx: Context): boolean {
/** Verify an unsafe request's token, origin, and browser fetch metadata. */
export function verifyCsrf(ctx: Context, options: CsrfProtectionOptions = {}): boolean {
if (SAFE_METHODS.has(ctx.req.method.toUpperCase())) return true;
if (options.verifyFetchMetadata !== false) {
const site = ctx.req.headers.get("sec-fetch-site");
if (site === "cross-site") return false;
}
if (options.verifyOrigin !== false) {
const origin = ctx.req.headers.get("origin");
if (origin) {
const trusted = new Set([ctx.url.origin, ...(options.trustedOrigins ?? [])]);
if (!trusted.has(origin)) return false;
}
}
const cookie = ctx.cookies.get(CSRF_COOKIE);
const sent = ctx.req.headers.get(CSRF_HEADER) ?? (ctx.locals._csrf as string | undefined);
return !!cookie && !!sent && timingSafeEqual(cookie, sent);
}
/**
* Constant-time string comparison — the running time does not depend on where
* the first differing byte is, so an attacker can't time-probe the token.
*/
/** Constant-time string comparison. */
function timingSafeEqual(a: string, b: string): boolean {
let diff = a.length ^ b.length;
const max = Math.max(a.length, b.length);
for (let i = 0; i < max; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
for (let i = 0; i < max; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
/** Middleware that 403s unsafe requests with a missing/mismatched CSRF token. */
export function csrfProtection(): Middleware {
/** Middleware that 403s unsafe requests with a missing/mismatched token. */
export function csrfProtection(options: CsrfProtectionOptions = {}): Middleware {
return (ctx, next) =>
verifyCsrf(ctx) ? next() : new Response("Invalid CSRF token", { status: 403 });
verifyCsrf(ctx, options) ? next() : new Response("Invalid CSRF token", { status: 403 });
}