fix: reject RPC requests without CSRF tokens
Quality / quality (ubuntu-latest) (push) Failing after 11m28s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-12 19:49:58 +05:30
parent f68f79786f
commit afe1c413cc
5 changed files with 53 additions and 16 deletions
+2 -2
View File
@@ -268,7 +268,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.23", "version": "0.8.24",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -332,7 +332,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.22", "version": "0.8.23",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.23", "version": "0.8.24",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.22", "version": "0.8.23",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+13 -12
View File
@@ -57,6 +57,18 @@ export { getWrnCompileMetrics, resetWrnCompileMetrics } from "./pipeline.ts";
export type { WrnCompileMetrics } from "./pipeline.ts"; export type { WrnCompileMetrics } from "./pipeline.ts";
import { resetDevCache } from "./cache.ts"; import { resetDevCache } from "./cache.ts";
export function validateRpcCsrf(request: Request): boolean {
const url = new URL(request.url);
const origin = request.headers.get("origin");
if (origin && origin !== url.origin) return false;
const cookieHeader = request.headers.get("cookie") ?? "";
const cookieToken =
/(?:^|;\s*)wrn-csrf=([^;]+)/.exec(cookieHeader)?.[1] ??
/(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(cookieHeader)?.[1];
const headerToken = request.headers.get("x-csrf-token") ?? request.headers.get("x-wrnexus-csrf");
return Boolean(cookieToken && headerToken && decodeURIComponent(cookieToken) === headerToken);
}
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types"; import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin"; import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin";
import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles"; import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles";
@@ -561,18 +573,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
} }
return null; return null;
}, },
validateCsrf(request) { validateCsrf: validateRpcCsrf,
const url = new URL(request.url);
const origin = request.headers.get("origin");
if (origin && origin !== url.origin) return false;
const cookieHeader = request.headers.get("cookie") ?? "";
const cookieToken =
/(?:^|;\s*)wrn-csrf=([^;]+)/.exec(cookieHeader)?.[1] ??
/(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(cookieHeader)?.[1];
const headerToken =
request.headers.get("x-csrf-token") ?? request.headers.get("x-wrnexus-csrf") ?? "";
return !cookieToken || decodeURIComponent(cookieToken) === headerToken;
},
}); });
const server = Bun.serve<WsData>({ const server = Bun.serve<WsData>({
+36
View File
@@ -0,0 +1,36 @@
import { expect, test } from "bun:test";
import { validateRpcCsrf } from "../src/index.ts";
function request(headers: Record<string, string> = {}): Request {
return new Request("https://app.example/__wrnexus/rpc", {
method: "POST",
headers,
});
}
test("RPC CSRF requires a matching cookie and header from the same origin", () => {
expect(validateRpcCsrf(request())).toBe(false);
expect(validateRpcCsrf(request({ cookie: "wrn-csrf=token" }))).toBe(false);
expect(validateRpcCsrf(request({ "x-csrf-token": "token" }))).toBe(false);
expect(validateRpcCsrf(request({ cookie: "wrn-csrf=token", "x-csrf-token": "forged" }))).toBe(
false,
);
expect(
validateRpcCsrf(
request({
cookie: "wrn-csrf=token",
"x-csrf-token": "token",
origin: "https://evil.example",
}),
),
).toBe(false);
expect(
validateRpcCsrf(
request({
cookie: "wrn-csrf=encoded%20token",
"x-csrf-token": "encoded token",
origin: "https://app.example",
}),
),
).toBe(true);
});