Skip to content

Vercel preset: /_vercel/cron handler is fully public when CRON_SECRET is unset — consider a fail-closed default or build-time warning #4546

Description

@zz-protocol

Description

While reviewing the Vercel preset I noticed the auto-registered cron handler only authenticates when CRON_SECRET happens to be set:

// src/presets/vercel/runtime/cron-handler.ts (main @ e36e7a6, unchanged as of 2026-08-21)
const cronSecret = process.env.CRON_SECRET;
if (cronSecret) {
  // ... timingSafeEqual check, 401 on mismatch
}
// no else — with no CRON_SECRET the request proceeds unauthenticated
const cron = event.req.headers.get("x-vercel-cron-schedule");
if (!cron) throw new HTTPError("Missing x-vercel-cron-schedule header", { status: 400 });
await runCronTasks(cron, { ... });
return { success: true };

Concretely, when experimental.tasks is enabled with non-empty scheduledTasks and CRON_SECRET is unset:

  • /_vercel/cron answers to any HTTP verb from anyone (the handler registers without a method, so rou3 matches all verbs);
  • an attacker chooses the schedule via x-vercel-cron-schedule, and runCronTasks exact-matches it against the configured keys — so any configured scheduled task can be executed on demand, repeatedly;
  • the { success: true } response confirms execution (400 if the header is missing; 401 only when CRON_SECRET is set);
  • minor extra leak: runTask is single-flight via __runningTasks__ (src/runtime/internal/task.ts:23-25), so a request timed while the real scheduled run is in-flight resolves with that run's task result value, which is returned to the caller if tasks return data.

There are currently no functional tests for this handler at all (test/presets/vercel.test.ts only snapshots the config output), so neither the 401 path nor the public-by-default path is pinned by tests.

I understand this mirrors Vercel's documented securing model (the code comment cites vercel.com/docs "securing cron jobs", and the nitro docs recommend setting CRON_SECRET), so I'm filing this as a hardening suggestion rather than a vulnerability claim. Still, the current default is fail-open for a code-execution endpoint, and nothing at build time or in the runtime output warns the deployer; a user who skips one env var silently publishes their scheduled tasks.

Suggestions (any one would close the gap)

  1. Fail closed: return 401/403 when CRON_SECRET is unset. (Would be a behavior change; a major-version candidate.)
  2. Build-time warning/error: when the preset registers /_vercel/cron without CRON_SECRET present, emit a prominent build warning (or refuse if strict).
  3. Docs: state explicitly in docs/2.deploy/20.providers/vercel.md that the endpoint is public by default without the secret.
  4. Tests: there are currently no tests covering the handler's auth behavior; a case for the unset-secret path would pin whatever default you choose.

Happy to send a PR for options 2-4. Context: verified against main @ e36e7a6; no live deployments were tested.


Reported by zz-protocol

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

    documentationImprovements or additions to documentationv3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions