diff --git a/.jules/sentinel.md b/.jules/sentinel.md index bda430635..894e12c28 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,4 +1,4 @@ -# Sentinel's Journal 🛡️ +# Sentinel's Journal 🛡 invaders and guardians of MiGrid ## 2025-05-16 - [IDOR in Multi-tenant Microservices] **Vulnerability:** Insecure Direct Object Reference (IDOR) and cross-fleet data enumeration. @@ -59,3 +59,8 @@ **Vulnerability:** Over-mocking middleware (such as bypass-decoding JWTs without verifying cryptographic signatures in unit tests). **Learning:** Mocking the authentication middleware to directly decode the payload without verifying signatures degrades the integrity of the security test suite. If verification logic breaks in production, the tests would still pass false-positively. **Prevention:** Ensure that the actual middleware executes cryptographic verification in tests by setting proper secure secrets (`process.env.JWT_SECRET`) in the test environment, rather than mocking verification away. + +## 2026-07-24 - [Insecure Default and Weak JWT Secrets in Production] +**Vulnerability:** Authentication mechanisms could default to or accept known weak secrets (`dev_secret_change_in_production`, `secret`, `test_secret`) in production mode. +**Learning:** Relying on developers to correctly set high-entropy secrets in production leaves applications vulnerable to offline token fabrication attacks if default values are deployed. +**Prevention:** Always enforce a dynamic configuration gate at application startup and middleware run-time to automatically reject default or weak secrets when `NODE_ENV` is set to `production`. diff --git a/services/05-driver-experience-api/index.js b/services/05-driver-experience-api/index.js index 26b7b4f25..c17035c88 100644 --- a/services/05-driver-experience-api/index.js +++ b/services/05-driver-experience-api/index.js @@ -24,6 +24,14 @@ notifications.init(pool); const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_change_in_production'; +// [Security] Weak secret definitions +const WEAK_SECRETS = ['dev_secret_change_in_production', 'test_secret', 'dev_secret', 'default_secret', 'secret']; + +const isWeakSecret = (secret) => { + if (!secret) return true; + return WEAK_SECRETS.includes(secret.toLowerCase().trim()); +}; + // Kafka Setup for broadcasting VPP opt-out events const kafka = new Kafka({ clientId: 'driver-experience-api', @@ -110,6 +118,12 @@ const authenticateToken = (req, res, next) => { if (!token) return res.status(401).json({ error: 'Access token required' }); + // [Security Hardening] Reject weak secrets in production environment + if (process.env.NODE_ENV === 'production' && isWeakSecret(JWT_SECRET)) { + console.error('[Security] JWT_SECRET is weak, insecure, or default. Blocking authenticated endpoint access in production.'); + return res.status(500).json({ error: 'Internal server configuration error' }); + } + jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid or expired token' }); req.user = user; @@ -204,6 +218,12 @@ app.post('/auth/register', registrationRateLimiter, async (req, res) => { // Login (With Security Sanitization) app.post('/auth/login', loginRateLimiter, async (req, res) => { + // [Security Hardening] Reject weak secrets in production environment + if (process.env.NODE_ENV === 'production' && isWeakSecret(JWT_SECRET)) { + console.error('[Security] JWT_SECRET is weak, insecure, or default. Blocking login in production.'); + return res.status(500).json({ error: 'Internal server configuration error' }); + } + const { email, password } = req.body; try { const result = await pool.query(` @@ -496,14 +516,18 @@ app.post('/voice/command', authenticateToken, async (req, res) => { } }); -// Start server -app.listen(port, () => { - console.log(`[Driver Experience API] Running on port ${port}`); - console.log('[Phase 5 Updates] Version 4.1.0 Synchronization Active'); -}); +// Start server if run directly +if (require.main === module) { + app.listen(port, () => { + console.log(`[Driver Experience API] Running on port ${port}`); + console.log('[Phase 5 Updates] Version 4.1.0 Synchronization Active'); + }); +} process.on('SIGTERM', async () => { if (kafkaConnected) await producer.disconnect(); pool.end(); process.exit(0); }); + +module.exports = { app }; diff --git a/services/05-driver-experience-api/security.test.js b/services/05-driver-experience-api/security.test.js new file mode 100644 index 000000000..cd61ab678 --- /dev/null +++ b/services/05-driver-experience-api/security.test.js @@ -0,0 +1,116 @@ +const request = require('supertest'); +const jwt = require('jsonwebtoken'); + +// Mock pg +const mockPool = { + connect: jest.fn(), + query: jest.fn(), + end: jest.fn(), + on: jest.fn() +}; +jest.mock('pg', () => { + return { Pool: jest.fn(() => mockPool) }; +}); + +// Mock kafkajs +const mockProducer = { + connect: jest.fn(), + send: jest.fn(), + disconnect: jest.fn() +}; +const mockKafka = { + producer: jest.fn(() => mockProducer) +}; +jest.mock('kafkajs', () => { + return { Kafka: jest.fn(() => mockKafka) }; +}); + +// Mock notifications to prevent setInterval from keeping Jest open +jest.mock('./notifications', () => ({ + init: jest.fn(), + queueForBatch: jest.fn(), + sendImmediate: jest.fn() +})); + +describe('L5 Driver Experience API Security Hardening', () => { + let originalEnv; + + beforeAll(() => { + originalEnv = { ...process.env }; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + jest.resetModules(); + }); + + test('GET /health should return 200 and run successfully', async () => { + const { app } = require('./index'); + const res = await request(app).get('/health'); + expect(res.status).toBe(200); + expect(res.body.service).toBe('driver-experience-api'); + }); + + test('POST /auth/login should fail securely with 500 when NODE_ENV is production and JWT_SECRET is default', async () => { + process.env.NODE_ENV = 'production'; + delete process.env.JWT_SECRET; // This forces default to be used + + const { app } = require('./index'); + const res = await request(app) + .post('/auth/login') + .send({ email: 'test@example.com', password: 'password123' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server configuration error'); + }); + + test('Authenticated route should fail securely with 500 when NODE_ENV is production and JWT_SECRET is weak', async () => { + process.env.NODE_ENV = 'production'; + process.env.JWT_SECRET = 'secret'; // weak secret + + const { app } = require('./index'); + const token = jwt.sign({ driver_id: 'driver-123', fleet_id: 'fleet-abc' }, 'secret'); + + const res = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${token}`); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server configuration error'); + }); + + test('Authenticated route should verify token correctly when NODE_ENV is production and JWT_SECRET is strong', async () => { + process.env.NODE_ENV = 'production'; + const strongSecret = 'super_strong_unpredictable_production_secret_key_12345'; + process.env.JWT_SECRET = strongSecret; + + mockPool.query.mockResolvedValueOnce({ + rows: [{ + id: 'driver-123', + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + fleet_id: 'fleet-abc', + is_plug_and_charge_ready: false, + escrow_balance: 0, + blockchain_balance: 0, + open_wallet_address: 'OW-address', + preferred_billing_mode: 'FLEET', + min_target_soc: 20, + target_departure_time: '08:00', + vpp_participation_active: true + }] + }); + + const { app } = require('./index'); + const token = jwt.sign({ driver_id: 'driver-123', fleet_id: 'fleet-abc' }, strongSecret); + + const res = await request(app) + .get('/auth/me') + .set('Authorization', `Bearer ${token}`); + + expect(res.status).not.toBe(500); + expect(res.status).toBe(200); + expect(res.body.id).toBe('driver-123'); + }); +});