From e4eaad6b07e6eb996ade67b9c73d54decc63e7d5 Mon Sep 17 00:00:00 2001 From: Martin Grolmus Date: Mon, 11 May 2026 15:44:20 +0200 Subject: [PATCH] fix(core): Refuse default superadmin credentials in production Vendure shipped with hardcoded default `superadmin` / `superadmin` credentials and no built-in guard against using them in production. Add a bootstrap-time check in `AdministratorService.ensureSuperAdminExists` that inspects `authOptions.superadminCredentials` against the well-known defaults from `@vendure/common`. The check: - throws (refusing to start) when `NODE_ENV === 'production'` and either the identifier or the password matches the default; - logs a `Logger.warn` with remediation hints in development / staging / any other non-test environment; - stays silent in `NODE_ENV === 'test'` to keep the test runner output clean (the e2e suite uses the defaults by design). The check function is extracted to `helpers/utils/check-superadmin-credentials.ts` and covered by unit tests. Existing 819 core unit tests still pass; no schema or public-API change. Reported privately via responsible disclosure (Linear OSS-490). --- .../check-superadmin-credentials.spec.ts | 57 +++++++++++++++++++ .../utils/check-superadmin-credentials.ts | 45 +++++++++++++++ .../service/services/administrator.service.ts | 3 + 3 files changed, 105 insertions(+) create mode 100644 packages/core/src/service/helpers/utils/check-superadmin-credentials.spec.ts create mode 100644 packages/core/src/service/helpers/utils/check-superadmin-credentials.ts diff --git a/packages/core/src/service/helpers/utils/check-superadmin-credentials.spec.ts b/packages/core/src/service/helpers/utils/check-superadmin-credentials.spec.ts new file mode 100644 index 0000000000..3bf5081922 --- /dev/null +++ b/packages/core/src/service/helpers/utils/check-superadmin-credentials.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { checkSuperadminCredentials } from './check-superadmin-credentials'; + +const DEFAULTS = { identifier: 'superadmin', password: 'superadmin' }; +const SAFE = { identifier: 'admin@example.com', password: 'a-very-long-random-passphrase' }; + +describe('checkSuperadminCredentials', () => { + it('throws in production when both identifier and password are default', () => { + expect(() => checkSuperadminCredentials(DEFAULTS, { nodeEnv: 'production' })).toThrow( + /Refusing to start/, + ); + }); + + it('throws in production when only the password is default', () => { + expect(() => + checkSuperadminCredentials( + { identifier: 'admin@example.com', password: 'superadmin' }, + { nodeEnv: 'production' }, + ), + ).toThrow(/Refusing to start/); + }); + + it('throws in production when only the identifier is default', () => { + expect(() => + checkSuperadminCredentials( + { identifier: 'superadmin', password: 'something-strong-1234' }, + { nodeEnv: 'production' }, + ), + ).toThrow(/Refusing to start/); + }); + + it('warns in development when defaults are used', () => { + const logger = { warn: vi.fn() }; + checkSuperadminCredentials(DEFAULTS, { nodeEnv: 'development', logger }); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn.mock.calls[0][0]).toMatch(/Default superadmin credentials/); + }); + + it('warns in staging / non-production environments when defaults are used', () => { + const logger = { warn: vi.fn() }; + checkSuperadminCredentials(DEFAULTS, { nodeEnv: 'staging', logger }); + expect(logger.warn).toHaveBeenCalledTimes(1); + }); + + it('is silent in test environment even when defaults are used', () => { + const logger = { warn: vi.fn() }; + checkSuperadminCredentials(DEFAULTS, { nodeEnv: 'test', logger }); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('is silent (and does not throw) when credentials are non-default in production', () => { + const logger = { warn: vi.fn() }; + expect(() => checkSuperadminCredentials(SAFE, { nodeEnv: 'production', logger })).not.toThrow(); + expect(logger.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/service/helpers/utils/check-superadmin-credentials.ts b/packages/core/src/service/helpers/utils/check-superadmin-credentials.ts new file mode 100644 index 0000000000..048ee3e621 --- /dev/null +++ b/packages/core/src/service/helpers/utils/check-superadmin-credentials.ts @@ -0,0 +1,45 @@ +import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants'; + +import { Logger } from '../../../config/logger/vendure-logger'; +import { SuperadminCredentials } from '../../../config/vendure-config'; + +const REMEDIATION_HINT = + 'Set `authOptions.superadminCredentials` in your VendureConfig — typically wired from environment variables, e.g. ' + + '`{ identifier: process.env.SUPERADMIN_USERNAME, password: process.env.SUPERADMIN_PASSWORD }`.'; + +/** + * @description + * Verifies that the configured `superadminCredentials` are not the well-known + * defaults shipped by `@vendure/common`. Used during bootstrap to fail loudly + * in production environments and warn otherwise. + * + * Exported for unit testing — production callers should rely on the default + * `process.env.NODE_ENV` and {@link Logger}. + */ +export function checkSuperadminCredentials( + credentials: Pick, + options: { nodeEnv?: string; logger?: Pick } = {}, +): void { + const usingDefaults = + credentials.identifier === SUPER_ADMIN_USER_IDENTIFIER || + credentials.password === SUPER_ADMIN_USER_PASSWORD; + if (!usingDefaults) { + return; + } + + const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV; + const message = + 'Default superadmin credentials are configured. This is INSECURE and must not be used in production. ' + + REMEDIATION_HINT; + + if (nodeEnv === 'production') { + throw new Error(`[Vendure] Refusing to start: ${message}`); + } + + if (nodeEnv === 'test') { + return; + } + + const logger = options.logger ?? Logger; + logger.warn(message); +} diff --git a/packages/core/src/service/services/administrator.service.ts b/packages/core/src/service/services/administrator.service.ts index 3861293241..6496fa0e81 100644 --- a/packages/core/src/service/services/administrator.service.ts +++ b/packages/core/src/service/services/administrator.service.ts @@ -26,6 +26,7 @@ import { CustomFieldRelationService } from '../helpers/custom-field-relation/cus import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder'; import { PasswordCipher } from '../helpers/password-cipher/password-cipher'; import { RequestContextService } from '../helpers/request-context/request-context.service'; +import { checkSuperadminCredentials } from '../helpers/utils/check-superadmin-credentials'; import { getChannelPermissions } from '../helpers/utils/get-user-channels-permissions'; import { patchEntity } from '../helpers/utils/patch-entity'; @@ -322,6 +323,8 @@ export class AdministratorService { private async ensureSuperAdminExists() { const { superadminCredentials } = this.configService.authOptions; + checkSuperadminCredentials(superadminCredentials); + const superAdminUser = await this.connection.rawConnection.getRepository(User).findOne({ where: { identifier: superadminCredentials.identifier,