Skip to content

HEAD requests bypass the server function method gate and execute any registered function #3069

Description

@frenzzy

Describe the bug

handleServerFunctionRequest gates GET dispatch on the GET(fn) declaration and answers 405
otherwise — "no crafted GET URLs against functions that never opted in". The check is
request.method === "GET" (packages/web/server-functions/src/server.ts:1626 on next), so a
HEAD request skips it entirely: any registered function id can be invoked, with arguments
from ?args=, whether or not it ever declared GET — including functions that only exist to
mutate.

HEAD is defined as safe and idempotent, and it is sent by link checkers, uptime probes,
prefetchers and proxies warming a URL, so this is reachable without anyone intending to call
the function.

Steps to reproduce

mkdir sf-repro && cd sf-repro && npm init -y
npm i @solidjs/web@2.0.0-rc.3          # pulls solid-js@2.0.0-rc.3
node --conditions=development repro.mjs  # the dev condition only makes the error text readable

repro.mjs:

import { configureServerFunctionsServer, handleServerFunctionRequest, registerServerReference } from "@solidjs/web/server-functions";
import { provideRequestEvent } from "@solidjs/web/storage";

configureServerFunctionsServer({ provideEvent: (event, run) => provideRequestEvent(event, run) });

let ran = false;
registerServerReference("post-only-0", async () => {
  ran = true;                       // stands in for a mutation
  return "side effect happened";
});

const headers = { "Sec-Fetch-Site": "same-origin" };
for (const method of ["HEAD", "GET"]) {
  ran = false;
  const res = await handleServerFunctionRequest(
    new Request("http://app.test/_server?id=post-only-0", { method, headers })
  );
  console.log(method, res.status, await res.text(), "executed:", ran);
}

Output on @solidjs/web@2.0.0-rc.3 (with solid-js@2.0.0-rc.3):

HEAD 200 side effect happened executed: true
GET 405 Method not allowed for server function: post-only-0 executed: false

Arguments come along too: HEAD /_server?id=echo-0&args=%5B%22from%20HEAD%22%5D reaches the
function as ["from HEAD"], because parseArguments reads the query whenever the
X-Server-Function-Instance header is absent.

Expected behavior

HEAD should be gated exactly like GET: 405 for functions that never declared GET, dispatch
for those that did (with the body dropped by the runtime, as HEAD requires). Today the method
enforcement added for GET is bypassable by changing one word of the request line.

Environment

@solidjs/web 2.0.0-rc.3 (published)
solid-js 2.0.0-rc.3
Node v24.19.0
OS macOS (darwin 25.6.0)
Code references next @ f5939ef

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions