From 516796ad969c0f01e36aa060f0fd5df7b93ca67c Mon Sep 17 00:00:00 2001 From: akordavid373 Date: Sat, 27 Jun 2026 12:44:15 +0100 Subject: [PATCH] security: replace env-or-default patterns with fail-closed requireEnv helper (#441, #444) - Remove hardcoded fallback REVALIDATE_WEBHOOK_SECRET from revalidate route - Create centralized requireEnv/requireEnvStrict helpers in src/lib/requireEnv.ts - Add test suite for requireEnv helpers - requireEnvStrict throws in all environments when env var is missing - requireEnv provides optional default with dev-only warning --- src/app/api/revalidate/route.ts | 4 +- src/lib/__tests__/requireEnv.test.ts | 64 ++++++++++++++++++++++++++++ src/lib/requireEnv.ts | 20 +++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/lib/__tests__/requireEnv.test.ts create mode 100644 src/lib/requireEnv.ts diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 732a3b76..a9a625a6 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -1,10 +1,10 @@ import { logger } from '@/utils/logger'; import { NextRequest, NextResponse } from 'next/server'; import { revalidateProperty, revalidateAllProperties } from '@/lib/propertyServiceServer'; +import { requireEnvStrict } from '@/lib/requireEnv'; import crypto from 'crypto'; -// Webhook secret for security - should be stored in environment variables -const WEBHOOK_SECRET = process.env.REVALIDATE_WEBHOOK_SECRET || 'your-webhook-secret'; +const WEBHOOK_SECRET = requireEnvStrict('REVALIDATE_WEBHOOK_SECRET'); export async function POST(request: NextRequest) { try { diff --git a/src/lib/__tests__/requireEnv.test.ts b/src/lib/__tests__/requireEnv.test.ts new file mode 100644 index 00000000..45ebfa95 --- /dev/null +++ b/src/lib/__tests__/requireEnv.test.ts @@ -0,0 +1,64 @@ +import { requireEnv, requireEnvStrict } from '../requireEnv'; + +const OLD_ENV = process.env; + +beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; +}); + +afterAll(() => { + process.env = OLD_ENV; +}); + +describe('requireEnv', () => { + it('returns the env value when present', () => { + process.env.MY_VAR = 'hello'; + expect(requireEnv('MY_VAR')).toBe('hello'); + }); + + it('returns the default when env is missing', () => { + delete process.env.MY_VAR; + expect(requireEnv('MY_VAR', 'fallback')).toBe('fallback'); + }); + + it('returns empty string and warns when env is missing in development', () => { + process.env.NODE_ENV = 'development'; + delete process.env.MY_VAR; + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const result = requireEnv('MY_VAR'); + expect(result).toBe(''); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Missing required environment variable: MY_VAR') + ); + warnSpy.mockRestore(); + }); + + it('returns empty string when env is empty string', () => { + process.env.MY_VAR = ''; + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + expect(requireEnv('MY_VAR')).toBe(''); + warnSpy.mockRestore(); + }); +}); + +describe('requireEnvStrict', () => { + it('returns the env value when present', () => { + process.env.STRICT_VAR = 'secret'; + expect(requireEnvStrict('STRICT_VAR')).toBe('secret'); + }); + + it('throws when env is missing', () => { + delete process.env.STRICT_VAR; + expect(() => requireEnvStrict('STRICT_VAR')).toThrow( + 'Missing required environment variable: STRICT_VAR' + ); + }); + + it('throws when env is empty', () => { + process.env.STRICT_VAR = ''; + expect(() => requireEnvStrict('STRICT_VAR')).toThrow( + 'Missing required environment variable: STRICT_VAR' + ); + }); +}); diff --git a/src/lib/requireEnv.ts b/src/lib/requireEnv.ts new file mode 100644 index 00000000..0966deae --- /dev/null +++ b/src/lib/requireEnv.ts @@ -0,0 +1,20 @@ +export function requireEnv(name: string, defaultValue?: string): string { + const value = process.env[name] ?? defaultValue; + if (value === undefined || value === '') { + const message = `Missing required environment variable: ${name}`; + if (process.env.NODE_ENV === 'production') { + throw new Error(message); + } + console.warn(`[requireEnv] ${message} — using fallback`); + return ''; + } + return value; +} + +export function requireEnvStrict(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +}