mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
fix: keep the desktop app's session secret and admin password out of its server's environment
This commit is contained in:
@@ -6,6 +6,7 @@ import { assertWritable, isConfigReadOnly } from './paths';
|
||||
import { adminConfigDocs, adminStateDocs, isStoreUnavailable } from './admin-store';
|
||||
import type { AdminConfigData, AdminStateData } from './types';
|
||||
import { readFileEnv } from '../read-file-env';
|
||||
import { desktopServerSecret } from '@/lib/desktop/server-secrets';
|
||||
|
||||
const SCRYPT_KEYLEN = 64;
|
||||
const SCRYPT_COST = 16384; // 2^14
|
||||
@@ -269,7 +270,7 @@ export async function initAdminPassword(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
const envPassword = process.env.ADMIN_PASSWORD;
|
||||
const envPassword = desktopServerSecret('adminPassword') || process.env.ADMIN_PASSWORD;
|
||||
if (!envPassword) {
|
||||
if (!cache.initialized) logger.info('Admin dashboard disabled (no ADMIN_PASSWORD set)');
|
||||
cache.initialized = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { configManager } from '@/lib/admin/config-manager';
|
||||
import { desktopServerSecret } from '@/lib/desktop/server-secrets';
|
||||
import { ensureStateDir, getStatePath } from '@/lib/admin/paths';
|
||||
import { readFileEnv } from '@/lib/read-file-env';
|
||||
import { getStore, isSharedMode } from '@/lib/shared-store';
|
||||
@@ -25,6 +26,9 @@ function sharedSecret(): string {
|
||||
|
||||
/** A secret the operator set: env, file, or the setup wizard's config. */
|
||||
function configuredSessionSecret(): string {
|
||||
const fromShell = desktopServerSecret('sessionSecret');
|
||||
if (fromShell) return fromShell;
|
||||
|
||||
const fromEnv = process.env.SESSION_SECRET;
|
||||
if (fromEnv) return fromEnv;
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* The desktop app's server gets its secrets by message, so they stay out of its
|
||||
* environment block. No other build looks there.
|
||||
*/
|
||||
|
||||
vi.mock('@/lib/admin/config-manager', () => ({
|
||||
configManager: { get: (_key: string, fallback: unknown) => fallback },
|
||||
}));
|
||||
vi.mock('@/lib/shared-store', () => ({ isSharedMode: () => false, getStore: () => ({}) }));
|
||||
vi.mock('@/lib/admin/paths', () => ({ ensureStateDir: async () => {}, getStatePath: () => 'C:/nowhere/.session-secret' }));
|
||||
|
||||
const KEY = Symbol.for('bulwark.desktop.serverSecrets');
|
||||
const holder = globalThis as unknown as Record<symbol, unknown>;
|
||||
const FROM_SHELL = 'from-the-shell-from-the-shell-from-the-shell';
|
||||
|
||||
async function sessionSecret() {
|
||||
vi.resetModules();
|
||||
return (await import('@/lib/auth/session-secret')).getSessionSecret();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
holder[KEY] = { sessionSecret: FROM_SHELL, adminPassword: 'admin-from-the-shell' };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
delete holder[KEY];
|
||||
});
|
||||
|
||||
describe('the session secret', () => {
|
||||
it('comes from the shell in the desktop build, with nothing in the environment', async () => {
|
||||
vi.stubEnv('NEXT_PUBLIC_BULWARK_DESKTOP', '1');
|
||||
vi.stubEnv('SESSION_SECRET', '');
|
||||
expect(await sessionSecret()).toBe(FROM_SHELL);
|
||||
});
|
||||
|
||||
it('never from there in a server build', async () => {
|
||||
vi.stubEnv('SESSION_SECRET', '');
|
||||
expect(await sessionSecret()).toBe('');
|
||||
vi.stubEnv('SESSION_SECRET', 'from-the-environment');
|
||||
expect(await sessionSecret()).toBe('from-the-environment');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the admin password', () => {
|
||||
it('is only readable from the shell in the desktop build', async () => {
|
||||
const { desktopServerSecret } = await import('../server-secrets');
|
||||
expect(desktopServerSecret('adminPassword')).toBe('');
|
||||
vi.stubEnv('NEXT_PUBLIC_BULWARK_DESKTOP', '1');
|
||||
expect(desktopServerSecret('adminPassword')).toBe('admin-from-the-shell');
|
||||
delete holder[KEY];
|
||||
expect(desktopServerSecret('adminPassword')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -134,6 +134,20 @@ describe('the flag is fixed at build time', () => {
|
||||
expect(await trustCheckOfBuild('1')).toContain(DESKTOP_TRUST_MARKER);
|
||||
});
|
||||
|
||||
it('the secrets from the shell are read in the desktop build only', async () => {
|
||||
const compile = async (flag: string) => (await build({
|
||||
entryPoints: [path.join(ROOT, 'lib/desktop/server-secrets.ts')],
|
||||
bundle: true,
|
||||
write: false,
|
||||
minify: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
define: { 'process.env.NEXT_PUBLIC_BULWARK_DESKTOP': JSON.stringify(flag) },
|
||||
})).outputFiles[0].text;
|
||||
expect(await compile('')).not.toContain('bulwark.desktop.serverSecrets');
|
||||
expect(await compile('1')).toContain('bulwark.desktop.serverSecrets');
|
||||
});
|
||||
|
||||
it('no image build sets it', () => {
|
||||
for (const file of ['Dockerfile', 'Dockerfile.lite']) {
|
||||
expect(readFileSync(path.join(ROOT, file), 'utf8')).not.toContain('NEXT_PUBLIC_BULWARK_DESKTOP');
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* The session secret and local admin password of the desktop app's own server.
|
||||
*
|
||||
* The Electron main process sends them by message, and src/server/host.ts in the
|
||||
* desktop repo puts them on globalThis, which all of Next's bundles share. They stay
|
||||
* out of the environment block, where a child process would inherit them.
|
||||
*
|
||||
* Every other build reads its secrets the usual way, and this compiles to nothing.
|
||||
*/
|
||||
|
||||
interface ShellSecrets {
|
||||
sessionSecret?: string;
|
||||
adminPassword?: string;
|
||||
}
|
||||
|
||||
export function desktopServerSecret(name: keyof ShellSecrets): string {
|
||||
if (process.env.NEXT_PUBLIC_BULWARK_DESKTOP !== '1') return '';
|
||||
const holder = globalThis as unknown as Record<symbol, ShellSecrets | undefined>;
|
||||
return holder[Symbol.for('bulwark.desktop.serverSecrets')]?.[name] ?? '';
|
||||
}
|
||||
Reference in New Issue
Block a user