mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
Several endpoints answered anyone with what fingerprints a deployment: `/api/health?detailed=true` (versions, memory), the update status (exact version and the text of a pending security advisory, even with `loginShowVersion` off), `/api/plugins` (installed plugins and the hosts they may reach), the admin policy (push relays, sidebar app URLs, plugin lists) and `/api/config` (every domain each server serves). Visitors without a session now get: - the plain liveness answer from `/api/health`; - the update status only where the login page shows the version, and never the advisory text; - a 401 from `/api/plugins`, which the client only syncs after signing in; - the policy without plugin lists, push relays and sidebar apps, marked `X-Bulwark-Policy-Scope: public`; the client fetches the full policy once it signs in; - server domain lists only when servers are picked by domain. A mailbox session, a remembered session cookie or the admin session counts as signed in. Deployments without a session secret cannot tell sessions apart and keep the old answers.
57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
|
|
const state = vi.hoisted(() => ({
|
|
secret: true,
|
|
context: false,
|
|
jar: new Map<string, string>(),
|
|
}));
|
|
|
|
vi.mock('@/lib/auth/session-secret', () => ({ hasSessionSecret: () => state.secret }));
|
|
vi.mock('@/lib/stalwart/credentials', () => ({
|
|
getStalwartCredentials: async () => (state.context ? { serverUrl: 'https://mail.example' } : null),
|
|
}));
|
|
vi.mock('next/headers', () => ({
|
|
cookies: async () => ({ get: (name: string) => (state.jar.has(name) ? { value: state.jar.get(name) } : undefined) }),
|
|
}));
|
|
vi.mock('@/lib/auth/crypto', () => ({ decryptSession: (token: string) => (token === 'valid' ? {} : null) }));
|
|
vi.mock('@/lib/admin/session', () => ({ verifyAdminSession: (token: string) => (token === 'admin' ? {} : null) }));
|
|
|
|
import { canSeeInstanceDetails } from '@/lib/auth/instance-details';
|
|
|
|
const request = new NextRequest('https://mail.example/api/plugins');
|
|
|
|
beforeEach(() => {
|
|
state.secret = true;
|
|
state.context = false;
|
|
state.jar.clear();
|
|
});
|
|
|
|
describe('canSeeInstanceDetails', () => {
|
|
it('refuses a visitor without a session', async () => {
|
|
expect(await canSeeInstanceDetails(request)).toBe(false);
|
|
state.jar.set('jmap_session', 'forged');
|
|
state.jar.set('admin_session', 'forged');
|
|
expect(await canSeeInstanceDetails(request)).toBe(false);
|
|
});
|
|
|
|
it('accepts a mailbox session, a remembered session cookie or the admin', async () => {
|
|
state.context = true;
|
|
expect(await canSeeInstanceDetails(request)).toBe(true);
|
|
|
|
state.context = false;
|
|
state.jar.set('jmap_session_2', 'valid');
|
|
expect(await canSeeInstanceDetails(request)).toBe(true);
|
|
|
|
state.jar.clear();
|
|
state.jar.set('admin_session', 'admin');
|
|
expect(await canSeeInstanceDetails(request)).toBe(true);
|
|
});
|
|
|
|
it('keeps answering everyone where no session secret exists to tell sessions apart', async () => {
|
|
state.secret = false;
|
|
expect(await canSeeInstanceDetails(request)).toBe(true);
|
|
});
|
|
});
|