fix: reject RPC requests without CSRF tokens
This commit is contained in:
@@ -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,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,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": {
|
||||||
|
|||||||
@@ -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>({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user