Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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`.
34 changes: 29 additions & 5 deletions services/05-driver-experience-api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace JWT secret bypasses gate

High Severity

The isWeakSecret check incorrectly treats whitespace-only JWT_SECRET values as strong in production. This bypasses the new security hardening for authentication and login, allowing an effectively empty and forgeable JWT secret to be used.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 72e5f2d. Configure here.


// Kafka Setup for broadcasting VPP opt-out events
const kafka = new Kafka({
clientId: 'driver-experience-api',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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' });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Login limiter before secret check

Medium Severity

The loginRateLimiter on POST /auth/login runs before the weak JWT_SECRET check. This means requests failing due to a weak secret configuration (returning a 500 error) still increment the rate limit, potentially leading to 429 errors that obscure the underlying configuration issue.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 72e5f2d. Configure here.


const { email, password } = req.body;
try {
const result = await pool.query(`
Expand Down Expand Up @@ -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 };
116 changes: 116 additions & 0 deletions services/05-driver-experience-api/security.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});