From b16ddfe724510a813d98e4ea7646d81969372f22 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 20 Aug 2026 20:56:55 +0200 Subject: [PATCH] test: cover server-function CSRF integration --- README.md | 13 +++++++++++++ examples/start-ssr/test/run.mjs | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/README.md b/README.md index f6bf925..4108999 100644 --- a/README.md +++ b/README.md @@ -654,6 +654,19 @@ production handler alike), and edits to the module hot-invalidate the handler in dev. Config calls merge per key, so it composes with the plugin's own runtime configuration. +Server-function requests are same-origin protected by `@solidjs/web`. To +allow another trusted origin, configure it in the same server-only module: + +```ts +import { configureServerFunctionsServer } from '@solidjs/web/server-functions/server'; + +configureServerFunctionsServer({ + csrf: { origin: ['https://app.example.com'] }, +}); +``` + +Set `csrf: false` only when another trusted layer protects the endpoint. + Meta-frameworks that need to control plugin ordering and dispatch requests through their own server should use the standalone `serverFunctions()` export instead, which never installs the dev middleware. See diff --git a/examples/start-ssr/test/run.mjs b/examples/start-ssr/test/run.mjs index c32ef6b..7fd9a70 100644 --- a/examples/start-ssr/test/run.mjs +++ b/examples/start-ssr/test/run.mjs @@ -129,6 +129,15 @@ import { resolveConfig, } from 'vite'; +const nativeFetch = globalThis.fetch; +function fetch(input, init) { + const request = new Request(input, init); + if (!request.headers.has('Sec-Fetch-Site')) { + request.headers.set('Sec-Fetch-Site', 'same-origin'); + } + return nativeFetch(request); +} + const exampleDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); // Deliberately no process.chdir(exampleDir): the in-process modes (external, // detect) create the dev server with `root: exampleDir` while the runner's @@ -297,6 +306,22 @@ function extractFunctionId(transformedCode, name) { return match ? match[1] : null; } +async function runCsrfChecks(mode, origin) { + const crossSite = await fetch(origin + '/_server?id=csrf-probe', { + method: 'POST', + headers: { 'Sec-Fetch-Site': 'cross-site' }, + }); + record( + mode, + 'csrf', + 'cross-site server function request rejected', + crossSite.status === 403, + ); + + const sameOrigin = await fetch(origin + '/_server?id=csrf-probe', { method: 'POST' }); + record(mode, 'csrf', 'same-origin request reaches dispatch', sameOrigin.status === 404); +} + // Distinctive rule from src/App.css: proves real styles (not just the dev // style patch) reached the page. Keep in sync with the stylesheet. const APP_CSS_COLOR = 'rgb(20, 40, 60)'; @@ -899,6 +924,7 @@ async function runDevMode() { ); const bogus = await fetch(origin + '/_server?id=bogus-0'); record(mode, 'sf', 'dev middleware rejects unknown id', bogus.status === 404); + await runCsrfChecks(mode, origin); const html = await runSsrChecks(mode, origin); record(mode, 'dev', 'Vite client injected into ', html.includes('/@vite/client')); @@ -1166,6 +1192,7 @@ async function runProdMode() { const bogus = await fetch(origin + '/_server?id=bogus-0'); record(mode, 'sf', 'prod handler rejects unknown id', bogus.status === 404); + await runCsrfChecks(mode, origin); const html = await runSsrChecks(mode, origin); record(