nitrojs / nitrojs/nitro

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

Open
#4,546 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

documentation v3
Dominant language
TypeScript
Stars
11.2k
Forks
899
Avg merge
2d 24m
Merged PRs (30d)
40

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/presets/vercel/runtime/cron-handler.ts and inspect how the handler behaves when CRON_SECRET is unset. Review test/presets/vercel.test.ts and the task path in src/runtime/internal/task.ts:23-25. Confirm the chosen hardening behavior with maintainers, then add coverage for the authentication behavior and update docs/2.deploy/20.providers/vercel.md if documentation is included.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, security, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.