Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions examples/start-ssr/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)';
Expand Down Expand Up @@ -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 <head>', html.includes('/@vite/client'));
Expand Down Expand Up @@ -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(
Expand Down
Loading