diff --git a/backend/src/middleware/__tests__/requireDeploymentAuth.test.ts b/backend/src/middleware/__tests__/requireDeploymentAuth.test.ts new file mode 100644 index 00000000..859d8736 --- /dev/null +++ b/backend/src/middleware/__tests__/requireDeploymentAuth.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +const getById = vi.fn(); +const verifyApiKey = vi.fn(); + +vi.mock('../../repositories/deploymentRepository.js', () => ({ + createDeploymentRepository: () => ({ getById }), + verifyApiKey: (...args: unknown[]) => verifyApiKey(...args), +})); + +vi.mock('../../repositories/projectRepository.js', () => ({ + getProjectRepository: () => ({}), +})); + +vi.mock('../../services/authService.js', () => ({ + authService: { verifyAccessToken: vi.fn() }, +})); + +vi.mock('../resourceOwnership.js', () => ({ + verifyProjectOwnership: vi.fn(), +})); + +import { requireDeploymentAuth } from '../requireDeploymentAuth.js'; + +function mockRes() { + const res: any = {}; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +} + +describe('requireDeploymentAuth enumeration resistance', () => { + beforeEach(() => { + getById.mockReset(); + verifyApiKey.mockReset(); + }); + + it('returns 404 Not found for missing deployment', async () => { + getById.mockResolvedValue(null); + const req: any = { params: { deploymentId: 'missing' }, headers: {} }; + const res = mockRes(); + const next = vi.fn(); + await requireDeploymentAuth(req, res, next); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: 'Not found' }); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns the same 404 Not found for a bad API key', async () => { + getById.mockResolvedValue({ deploymentId: 'dep-1', projectId: 'p1' }); + verifyApiKey.mockResolvedValue(null); + const req: any = { + params: { deploymentId: 'dep-1' }, + headers: { 'x-api-key': 'bad-key' }, + }; + const res = mockRes(); + const next = vi.fn(); + await requireDeploymentAuth(req, res, next); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ error: 'Not found' }); + expect(next).not.toHaveBeenCalled(); + }); +}); diff --git a/backend/src/middleware/requireDeploymentAuth.ts b/backend/src/middleware/requireDeploymentAuth.ts index 8854ed80..f547b615 100644 --- a/backend/src/middleware/requireDeploymentAuth.ts +++ b/backend/src/middleware/requireDeploymentAuth.ts @@ -45,8 +45,9 @@ export async function requireDeploymentAuth( const apiKey = req.headers['x-api-key'] as string | undefined; if (apiKey) { const keyRecord = await verifyApiKey(apiKey, deploymentRepo); + // Same 404 body as a missing deployment so API keys cannot enumerate IDs. if (!keyRecord || keyRecord.deploymentId !== deployment.deploymentId) { - res.status(403).json({ error: 'Forbidden' }); + res.status(404).json({ error: 'Not found' }); return; } req.deployment = deployment;