Skip to content

fix(cron): reject "Bearer undefined" when CRON_SECRET is unset#29569

Open
yashs33244 wants to merge 2 commits into
calcom:mainfrom
yashs33244:fix/cron-secret-bearer-undefined
Open

fix(cron): reject "Bearer undefined" when CRON_SECRET is unset#29569
yashs33244 wants to merge 2 commits into
calcom:mainfrom
yashs33244:fix/cron-secret-bearer-undefined

Conversation

@yashs33244

Copy link
Copy Markdown

Fixes #29565.

Summary

Several cron/tasker route handlers authorize by comparing the Authorization header to `Bearer ${process.env.CRON_SECRET}`. When CRON_SECRET is unset (it is not in .env.example), that literal becomes the string "Bearer undefined", so a request with Authorization: Bearer undefined is accepted. The array-based routes had the same issue via [CRON_API_KEY, \Bearer ${CRON_SECRET}`].includes(...)`.

This makes the checks fail-closed:

  • reject when the configured secret is unset (no more "Bearer undefined" match)
  • build the allow-list from defined values only (.filter(Boolean)) and reject a missing apiKey
  • document CRON_SECRET in .env.example and packages/types/environment.d.ts

Existing status codes / messages are preserved to keep the diff focused.

Tests

  • Added the tasker routes' first auth tests + a Bearer undefined regression test
  • auth.test.ts 7/7, calendar-subscriptions(-cleanup) 19/19, biome lint clean, tsc clean on changed files

Note

A review surfaced a related but separate misconfiguration class: four other cron routes can be bypassed if an operator explicitly sets CRON_API_KEY="". That is outside this issue's scope — happy to follow up in a separate PR if useful.

… is unset

The tasker cleanup/cron routes and the calendar-subscription cron routes
authorize by comparing the Authorization header to `Bearer ${process.env.CRON_SECRET}`.
CRON_SECRET is not in .env.example, so on a default deployment it is undefined
and the literal becomes the string "Bearer undefined". An unauthenticated caller
sending "Authorization: Bearer undefined" passes the check and can trigger
tasker.cleanup() (which deletes data) and queue processing. The array-based
calendar-subscription checks had the same flaw via
[CRON_API_KEY, `Bearer ${CRON_SECRET}`].includes(...).

Make every check fail closed when the secret is unset: guard the single-secret
routes on a missing CRON_SECRET, and build the calendar-subscription allow-list
from defined values only while rejecting a missing apiKey. Document CRON_SECRET
in .env.example and declare it in the env type file. Add the tasker routes' first
auth tests plus a 'Bearer undefined' regression test for the cleanup cron route.

Fixes calcom#29565
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @yashs33244! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions Bot added ❗️ .env changes contains changes to env variables 🐛 bug Something isn't working labels Jun 15, 2026
@yashs33244 yashs33244 marked this pull request as ready for review June 15, 2026 04:23
Copilot AI review requested due to automatic review settings June 15, 2026 04:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 118e97ce-f096-4c71-b139-265e957e1b45

📥 Commits

Reviewing files that changed from the base of the PR and between 212dfdc and 4f51692.

📒 Files selected for processing (3)
  • apps/web/app/api/cron/calendar-subscriptions/__tests__/route.test.ts
  • apps/web/app/api/cron/calendar-subscriptions/route.ts
  • packages/features/tasker/api/__tests__/auth.test.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/app/api/cron/calendar-subscriptions/tests/route.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/api/cron/calendar-subscriptions/route.ts
  • packages/features/tasker/api/tests/auth.test.ts

📝 Walkthrough

Walkthrough

The pull request introduces CRON_SECRET as an optional environment variable that acts as a bearer token for authorizing cron and tasker API endpoints. A new type declaration is added to NodeJS.ProcessEnv and the variable is documented in .env.example. Authorization guards in packages/features/tasker/api/cleanup.ts and cron.ts are updated to require CRON_SECRET to be present before accepting the Bearer token. Three web calendar cron routes (calendar-subscriptions-cleanup, calendar-subscriptions, and selected-calendars) update their validApiKeys construction to filter out entries derived from unset environment variables, preventing "Bearer undefined" from being accepted. New tests are added for the tasker auth handlers, an additional test case is added to the calendar-subscriptions-cleanup route tests, and test assertions in the calendar-subscriptions route are corrected from the misspelled "Forbiden" to "Forbidden".

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main security fix: rejecting the 'Bearer undefined' authorization string when CRON_SECRET is unset.
Description check ✅ Passed The description comprehensively explains the vulnerability, the fix strategy, testing approach, and known limitations, directly relating to the changeset.
Linked Issues check ✅ Passed The PR fully addresses issue #29565: rejects 'Bearer undefined' by checking if CRON_SECRET is set, filters array-based allow-lists with .filter(Boolean), documents CRON_SECRET in .env.example and types, and includes regression tests.
Out of Scope Changes check ✅ Passed All changes directly support the core fix: environment variable documentation, authorization logic updates, test coverage, and TypeScript type definitions. The PR explicitly excludes the related CRON_API_KEY="" bypass as outside scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/features/tasker/api/__tests__/auth.test.ts (1)

105-135: ⚡ Quick win

Complete cron route test coverage to match cleanup route.

The cleanup route has 4 test cases, but the cron route only has 3. The missing test is:

  • "rejects a request with no authorization header when CRON_SECRET is unset"

Adding this test would ensure consistent coverage across both routes and complete the authentication test matrix.

📝 Suggested test case
    test("rejects 'Bearer undefined' when CRON_SECRET is unset", async () => {
      vi.stubEnv("CRON_SECRET", undefined);
      const { GET } = await import("../cron");

      const response = await GET(buildRequest("Bearer undefined"));

      expect(response.status).toBe(401);
      expect(processQueueMock).not.toHaveBeenCalled();
    });

+   test("rejects a request with no authorization header when CRON_SECRET is unset", async () => {
+     vi.stubEnv("CRON_SECRET", undefined);
+     const { GET } = await import("../cron");
+
+     const response = await GET(buildRequest());
+
+     expect(response.status).toBe(401);
+     expect(processQueueMock).not.toHaveBeenCalled();
+   });

    test("accepts the correct Bearer token via POST when CRON_SECRET is set", async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/features/tasker/api/__tests__/auth.test.ts` around lines 105 - 135,
Add a missing test case to the "cron route" describe block to match the cleanup
route's test coverage. Create a new test that verifies rejection of a request
with no authorization header (completely absent, not just "Bearer undefined")
when CRON_SECRET is unset. This test should use buildRequest with no
authorization header (or an empty string), expect a 401 status response, and
verify that processQueueMock is not called. This test completes the
authentication matrix by covering the case of a completely missing authorization
header, complementing the existing test that checks for the "Bearer undefined"
token.
apps/web/app/api/cron/calendar-subscriptions-cleanup/__tests__/route.test.ts (1)

104-116: 💤 Low value

Consider clarifying test name to reflect both environment variables.

The test name mentions "when CRON_SECRET is unset," but the test also sets CRON_API_KEY to undefined (line 105). While the test is correct and comprehensively covers the default configuration scenario, the name could be more precise.

📝 Suggested name improvement
-   test("should return 403 for 'Bearer undefined' when CRON_SECRET is unset", async () => {
+   test("should return 403 for 'Bearer undefined' when CRON_SECRET and CRON_API_KEY are unset", async () => {
     vi.stubEnv("CRON_API_KEY", undefined);
     vi.stubEnv("CRON_SECRET", undefined);

Alternatively, if you want to test the scenario where only CRON_SECRET is unset:

    test("should return 403 for 'Bearer undefined' when CRON_SECRET is unset", async () => {
-     vi.stubEnv("CRON_API_KEY", undefined);
      vi.stubEnv("CRON_SECRET", undefined);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/cron/calendar-subscriptions-cleanup/__tests__/route.test.ts`
around lines 104 - 116, The test name for the function is misleading because it
only mentions "CRON_SECRET is unset" but the test actually unsets both
CRON_API_KEY and CRON_SECRET. Update the test name (the string parameter passed
to the test function) to accurately reflect that both environment variables are
being set to undefined, making the test's purpose clearer and more precise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/api/cron/calendar-subscriptions/route.ts`:
- Line 33: Fix the spelling error in the error message returned by
NextResponse.json at line 33. Change "Forbiden" to "Forbidden" in the message
field to correct the typo in the HTTP 403 error response.

In `@packages/features/tasker/api/__tests__/auth.test.ts`:
- Line 34: In cleanup.ts at line 4, replace the barrel import `import tasker
from ".."` with a direct import from the tasker-factory module: `import {
getTasker } from "../tasker-factory"`. Then update any references to the
`tasker` variable in the file to use `getTasker()` function calls instead to
obtain the tasker instance.

---

Nitpick comments:
In
`@apps/web/app/api/cron/calendar-subscriptions-cleanup/__tests__/route.test.ts`:
- Around line 104-116: The test name for the function is misleading because it
only mentions "CRON_SECRET is unset" but the test actually unsets both
CRON_API_KEY and CRON_SECRET. Update the test name (the string parameter passed
to the test function) to accurately reflect that both environment variables are
being set to undefined, making the test's purpose clearer and more precise.

In `@packages/features/tasker/api/__tests__/auth.test.ts`:
- Around line 105-135: Add a missing test case to the "cron route" describe
block to match the cleanup route's test coverage. Create a new test that
verifies rejection of a request with no authorization header (completely absent,
not just "Bearer undefined") when CRON_SECRET is unset. This test should use
buildRequest with no authorization header (or an empty string), expect a 401
status response, and verify that processQueueMock is not called. This test
completes the authentication matrix by covering the case of a completely missing
authorization header, complementing the existing test that checks for the
"Bearer undefined" token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca6a6fe1-8114-47ca-b877-9f24f747b4d3

📥 Commits

Reviewing files that changed from the base of the PR and between dcd0da8 and 212dfdc.

📒 Files selected for processing (9)
  • .env.example
  • apps/web/app/api/cron/calendar-subscriptions-cleanup/__tests__/route.test.ts
  • apps/web/app/api/cron/calendar-subscriptions-cleanup/route.ts
  • apps/web/app/api/cron/calendar-subscriptions/route.ts
  • apps/web/app/api/cron/selected-calendars/route.ts
  • packages/features/tasker/api/__tests__/auth.test.ts
  • packages/features/tasker/api/cleanup.ts
  • packages/features/tasker/api/cron.ts
  • packages/types/environment.d.ts

Comment thread apps/web/app/api/cron/calendar-subscriptions/route.ts Outdated

// The routes under test import from "..", which resolves to packages/features/tasker
// relative to those files. From this test file that module is "../../index".
vi.mock("../../index", () => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if cleanup.ts and cron.ts use barrel imports

echo "=== Checking imports in cleanup.ts ==="
rg -n "^import.*from ['\"]\.\.(/index)?(\.ts)?['\"]" packages/features/tasker/api/cleanup.ts

echo -e "\n=== Checking imports in cron.ts ==="
rg -n "^import.*from ['\"]\.\.(/index)?(\.ts)?['\"]" packages/features/tasker/api/cron.ts

echo -e "\n=== Full import blocks for context ==="
rg -n -A1 "^import" packages/features/tasker/api/cleanup.ts packages/features/tasker/api/cron.ts | head -20

Repository: calcom/cal.diy

Length of output: 873


🏁 Script executed:

#!/bin/bash
# Check what's exported from the index.ts barrel file
echo "=== Contents of packages/features/tasker/index.ts ==="
cat -n packages/features/tasker/index.ts

echo -e "\n=== Check all imports from '..' or barrel files in tasker/api ==="
rg -n "from ['\"]\.\.(/index)?(\.ts)?['\"]" packages/features/tasker/api/

echo -e "\n=== Check how 'tasker' is used in cleanup.ts ==="
rg -n "tasker\." packages/features/tasker/api/cleanup.ts

Repository: calcom/cal.diy

Length of output: 888


🏁 Script executed:

#!/bin/bash
# Check the structure of tasker-factory to understand the correct import path
echo "=== Contents of packages/features/tasker/tasker-factory.ts ==="
head -30 packages/features/tasker/tasker-factory.ts

echo -e "\n=== Check if there are other files in packages/features/tasker root ==="
ls -la packages/features/tasker/*.ts 2>/dev/null

Repository: calcom/cal.diy

Length of output: 1909


Replace barrel import in cleanup.ts with direct import from tasker-factory.

cleanup.ts line 4 imports from ".." (resolving to the barrel file index.ts), which violates the guideline to import directly from source files. Replace import tasker from ".." with import { getTasker } from "../tasker-factory" and call getTasker() to obtain the tasker instance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/features/tasker/api/__tests__/auth.test.ts` at line 34, In
cleanup.ts at line 4, replace the barrel import `import tasker from ".."` with a
direct import from the tasker-factory module: `import { getTasker } from
"../tasker-factory"`. Then update any references to the `tasker` variable in the
file to use `getTasker()` function calls instead to obtain the tasker instance.

Source: Coding guidelines

@CLAassistant

CLAassistant commented Jun 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Address review on calcom#29569:
- Fix the misspelled "Forbiden" 403 message in the calendar-subscriptions
  route to match the sibling cron routes; update the route tests accordingly.
- Add the missing 'no authorization header when CRON_SECRET is unset' case to
  the tasker cron auth tests so the cron and cleanup routes have symmetric
  coverage.
@yashs33244

Copy link
Copy Markdown
Author

Thanks for the review! Addressed in 4f51692:

  • ForbidenForbidden typo (calendar-subscriptions/route.ts): fixed, and updated the two route tests that asserted the old spelling, so it's consistent with the sibling cron routes.
  • Missing cron coverage: added the rejects a request with no authorization header when CRON_SECRET is unset case to the tasker cron tests, so the cron and cleanup routes now have symmetric coverage.

On the barrel-import note in cleanup.ts (import tasker from ".."getTasker()): I left this out of the current PR. index.ts exports a memoized global singleton (export const tasker = globalForTasker.tasker || getTasker()), whereas getTasker() builds a fresh instance per call — so swapping it is a behavioral change rather than a pure import-style fix, and it's unrelated to the Bearer undefined security fix. Happy to do the barrel-import cleanup as a separate focused PR.

Locally: tasker auth + calendar-subscriptions route tests pass (17/17), and the typo fix is red-green proven (reverting the source makes the two 403 assertions fail).

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

Labels

🐛 bug Something isn't working ❗️ .env changes contains changes to env variables size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cron/tasker endpoints authorize with "Bearer undefined" when CRON_SECRET is not set

3 participants