Vercel preset: `/_vercel/cron` handler is fully public when `CRON_SECRET` is unset — consider a fail-closed default or build-time warning
Nobody has claimed this yet.
- 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/cronanswers to any HTTP verb from anyone (the handler registers without amethod, so rou3 matches all verbs);- an attacker chooses the schedule via
x-vercel-cron-schedule, andrunCronTasksexact-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 whenCRON_SECRETis set); - minor extra leak:
runTaskis 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)
- Fail closed: return 401/403 when
CRON_SECRETis unset. (Would be a behavior change; a major-version candidate.) - Build-time warning/error: when the preset registers
/_vercel/cronwithoutCRON_SECRETpresent, emit a prominent build warning (or refuse ifstrict). - Docs: state explicitly in
docs/2.deploy/20.providers/vercel.mdthat the endpoint is public by default without the secret. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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