Skip to content

fix: fail closed when CRON_SECRET is absent in WakaTime sync endpoint#1780

Merged
Priyanshu-byte-coder merged 1 commit into
Priyanshu-byte-coder:mainfrom
Ridanshi:fix/wakatime-sync-cron-auth
May 31, 2026
Merged

fix: fail closed when CRON_SECRET is absent in WakaTime sync endpoint#1780
Priyanshu-byte-coder merged 1 commit into
Priyanshu-byte-coder:mainfrom
Ridanshi:fix/wakatime-sync-cron-auth

Conversation

@Ridanshi
Copy link
Copy Markdown
Contributor

Closes #1746

Problem

GET /api/wakatime/sync used process.env.CRON_SECRET directly inside a template literal without first checking whether the variable is defined:

if (authHeader !== `Bearer ${process.env.CRON_SECRET}` && process.env.NODE_ENV !== "development") {

When CRON_SECRET is not set, JavaScript coerces undefined to the string "undefined", so the expected credential becomes the literal string "Bearer undefined". Any caller who sends that header in production passes authentication and triggers:

  1. A full query of all users with stored WakaTime keys
  2. Decryption of every user's WakaTime API key
  3. A WakaTime API call for every user
  4. Database upserts for all results

Endpoint comparison

Every other cron endpoint in the repository already follows the correct pattern:

Endpoint Pattern
cron/weekly-digest if (!cronSecret) return 500 then compare
notifications/discord-sync if (!cronSecret) return 500 then compare
sponsors/sync if (!cronSecret) return 500 then compare
wakatime/sync ❌ inline process.env.CRON_SECRET without null check

Fix

Mirror the established pattern used by every other endpoint:

const cronSecret = process.env.CRON_SECRET;

// Fail closed — never allow "Bearer undefined" as a credential.
if (!cronSecret) {
  return NextResponse.json(
    { error: "CRON_SECRET is not configured" },
    { status: 500 }
  );
}

if (authHeader !== `Bearer ${cronSecret}` && process.env.NODE_ENV !== "development") {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

When CRON_SECRET is absent the endpoint now returns 500 before any string comparison occurs, so Authorization: Bearer undefined and every other header value are unconditionally rejected.

Development mode intentionally bypasses the header check (existing behavior, preserved); however, the !cronSecret guard now fires even in development, ensuring misconfigured environments are surfaced regardless of NODE_ENV.

Tests — test/wakatime-sync-auth.test.ts (10 new tests)

Test Purpose
Missing CRON_SECRET → 500 Fail-closed baseline
Bearer undefined with missing secret → non-200 Direct regression for #1746
Correct Bearer <secret> → 200 Happy path
Wrong secret in production → 401 Header rejection
No header in production → 401 Missing header rejection
Plaintext secret without Bearer → 401 Malformed header
Development: any header passes Intentional bypass preserved
Missing CRON_SECRET in development → 500 Fail closed even in dev
Decryption failure → counted as failure, not crash Error handling
Full happy-path: decrypt → WakaTime call → upsert Sync execution

All 10 pass. The one pre-existing failure in test/dateUtils.test.ts is a timezone boundary issue unrelated to this change.

Root cause
----------
The WakaTime sync endpoint used process.env.CRON_SECRET inline without
first checking whether it was defined:

  if (authHeader !== `Bearer ${process.env.CRON_SECRET}` && ...)

When CRON_SECRET is undefined, JavaScript coerces it to the string
"undefined", making "Bearer undefined" the expected credential. Any
caller who sends that literal header would pass the authentication check
and trigger a full WakaTime credential decryption and API sweep.

Every other cron endpoint in the codebase (cron/weekly-digest,
notifications/discord-sync, sponsors/sync) already follows the correct
pattern: extract the secret first, return 500 if it is absent, then
compare. This endpoint was the only exception.

Fix
---
Mirror the established pattern:

  const cronSecret = process.env.CRON_SECRET;
  if (!cronSecret) {
    return NextResponse.json(
      { error: "CRON_SECRET is not configured" },
      { status: 500 }
    );
  }
  if (authHeader !== `Bearer ${cronSecret}` && NODE_ENV !== "development") {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

When CRON_SECRET is absent the endpoint now returns 500 before any
credential comparison occurs, so "Bearer undefined" and every other
header value are rejected unconditionally.

test/wakatime-sync-auth.test.ts — 10 new tests:
- Missing CRON_SECRET → 500, Supabase never called
- Authorization: Bearer undefined with missing CRON_SECRET → non-200
  (regression test for Priyanshu-byte-coder#1746)
- Correct secret → 200 and sync executes
- Wrong secret in production → 401
- No Authorization header in production → 401
- Plaintext secret without Bearer prefix → 401
- Development mode: any header passes (existing intentional behavior)
- Missing CRON_SECRET in development → still 500 (fail closed)
- Decryption failure counts as a sync failure (not a crash)
- Full happy-path: user decrypted, WakaTime called, stats upserted

Closes Priyanshu-byte-coder#1746
@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

@Ridanshi is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:testing GSSoC type bonus: tests (+10 pts) labels May 31, 2026
@github-actions
Copy link
Copy Markdown

GSSoC Label Checklist 🏷️

@Priyanshu-byte-coder — please apply the appropriate labels before merging:

Difficulty (pick one):

  • level:beginner — 20 pts
  • level:intermediate — 35 pts
  • level:advanced — 55 pts
  • level:critical — 80 pts

Quality (optional):

  • quality:clean — ×1.2 multiplier
  • quality:exceptional — ×1.5 multiplier

Validation (required to score):

  • gssoc:approved — counts for points
  • gssoc:invalid / gssoc:spam / gssoc:ai-slop — does not score

Type labels (type:*) are auto-detected from files and title. Review and adjust if needed.
Points formula: (difficulty × quality_multiplier) + type_bonus

@Priyanshu-byte-coder Priyanshu-byte-coder added level2 GSSoC Level 2 - Medium complexity (25 points) gssoc:approved GSSoC: PR approved for scoring labels May 31, 2026
@Priyanshu-byte-coder Priyanshu-byte-coder merged commit 9cf5476 into Priyanshu-byte-coder:main May 31, 2026
4 of 5 checks passed
@github-actions
Copy link
Copy Markdown

🎉 Merged! Thanks for contributing to DevTrack.

If the project has been useful to you, a ⭐ star on the repo is the easiest way to support it — it helps DevTrack get discovered by more developers.

Keep an eye on open issues for your next contribution!

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

Labels

gssoc:approved GSSoC: PR approved for scoring gssoc26 GSSoC 2026 contribution level2 GSSoC Level 2 - Medium complexity (25 points) type:bug GSSoC type bonus: bug fix type:testing GSSoC type bonus: tests (+10 pts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] WakaTime synchronization endpoint can be executed using Authorization: Bearer undefined when CRON_SECRET is absent

2 participants