From 4ec701a5d9678ae113ae238b017d4c3ab65b1536 Mon Sep 17 00:00:00 2001 From: Storm 32190 Date: Tue, 19 May 2026 23:11:44 +0500 Subject: [PATCH] fix(security): harden default API key, IP whitelist, and QR access Seed random admin key unless ALLOW_DEV_API_KEY=true is set explicitly. Reject API keys with IP whitelist when client IP cannot be determined. Require OPERATOR role for GET /sessions/:id/qr. Document ALLOW_DEV_API_KEY in .env.example and docs README. Opt in predictable dev key in docker-compose.dev.yml. --- .env.example | 3 +++ docker-compose.dev.yml | 1 + docs/README.md | 4 ++-- src/modules/auth/auth.service.spec.ts | 10 ++++++++++ src/modules/auth/auth.service.ts | 13 +++++++++---- src/modules/session/session.controller.ts | 1 + 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index dd48168..a30eeab 100644 --- a/.env.example +++ b/.env.example @@ -113,6 +113,9 @@ PLUGINS_DIR=./data/plugins # Plugin directory # Master API key (leave empty to disable, or set to secure value) API_MASTER_KEY= +# First-boot default admin key: always random unless this is set (local dev only) +# ALLOW_DEV_API_KEY=true + # ============================================================================= # DEVELOPER SETTINGS # ============================================================================= diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c258bad..977c2f3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,6 +12,7 @@ services: - '127.0.0.1:2785:2785' environment: - NODE_ENV=development + - ALLOW_DEV_API_KEY=true - PORT=2785 - DATABASE_TYPE=sqlite - DATABASE_NAME=/app/data/openwa.sqlite diff --git a/docs/README.md b/docs/README.md index f6d95ba..2474166 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,9 +101,9 @@ Access: ### API Key -OpenWA seeds a default API key on first run and writes it to: +OpenWA seeds a random default admin API key on first run and writes it to `data/.api-key`. The key is printed in the server startup log. -- `data/.api-key` (development) +For local development only, you may set `ALLOW_DEV_API_KEY=true` to use the predictable key `dev-admin-key` (never enable this in production or shared environments). You can also create new keys via the API (see [API Specification](./06-api-specification.md)). diff --git a/src/modules/auth/auth.service.spec.ts b/src/modules/auth/auth.service.spec.ts index 44ce32b..60c0e6c 100644 --- a/src/modules/auth/auth.service.spec.ts +++ b/src/modules/auth/auth.service.spec.ts @@ -234,6 +234,16 @@ describe('AuthService', () => { expect(result.id).toBe(key.id); }); + it('should reject when IP whitelist is set but client IP is unknown', async () => { + const key = createMockApiKey({ + allowedIps: ['10.0.0.1'], + keyHash: hashKey('ip-no-client'), + }); + (repository.findOne as jest.Mock).mockResolvedValue(key); + + await expect(service.validateApiKey('ip-no-client')).rejects.toThrow('Client IP could not be determined'); + }); + it('should throw UnauthorizedException when session not in allowedSessions', async () => { const key = createMockApiKey({ allowedSessions: ['session-A'], diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 4b4d79f..d369636 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -26,9 +26,11 @@ export class AuthService implements OnModuleInit { let isNewKey = false; if (count === 0) { - // Use predictable key in development, random key in production + // Random by default; predictable key only when explicitly opted in for local dev displayKey = - process.env.NODE_ENV === 'production' ? `owa_k1_${randomBytes(32).toString('hex')}` : 'dev-admin-key'; + process.env.ALLOW_DEV_API_KEY === 'true' + ? 'dev-admin-key' + : `owa_k1_${randomBytes(32).toString('hex')}`; await this.seedApiKey(displayKey, 'Default Admin Key', ApiKeyRole.ADMIN); isNewKey = true; @@ -173,8 +175,11 @@ export class AuthService implements OnModuleInit { throw new UnauthorizedException('API key has expired'); } - // Check IP whitelist - if (apiKey.allowedIps && apiKey.allowedIps.length > 0 && clientIp) { + // Check IP whitelist (fail closed when configured but client IP is unknown) + if (apiKey.allowedIps && apiKey.allowedIps.length > 0) { + if (!clientIp) { + throw new UnauthorizedException('Client IP could not be determined'); + } if (!this.isIpAllowed(clientIp, apiKey.allowedIps)) { this.logger.warn(`IP not allowed: ${clientIp}`, { keyId: apiKey.id, diff --git a/src/modules/session/session.controller.ts b/src/modules/session/session.controller.ts index 5003c9a..0b0fe86 100644 --- a/src/modules/session/session.controller.ts +++ b/src/modules/session/session.controller.ts @@ -133,6 +133,7 @@ export class SessionController { } @Get(':id/qr') + @RequireRole(ApiKeyRole.OPERATOR) @ApiOperation({ summary: 'Get QR code for session authentication' }) @ApiParam({ name: 'id', description: 'Session ID' }) @ApiResponse({