FRE-4499: Fix security review findings (S01-S06)
- S01 (High): Pre-compile regex patterns in RuleEngine.loadActiveRules() and
cache them; eliminate per-evaluation RegExp construction in rule-engine.ts
and spamshield.service.ts (ReDoS mitigation)
- S02 (High): SMS classifier now accepts optional senderPhoneNumber via
SmsClassificationContext; reputation check uses actual sender instead of
hardcoded 'placeholder'
- S03 (Medium): AlertServer (services/spamshield) now enforces JWT auth,
origin allowlist, and max client limit on WebSocket connections
- S04 (Medium): hashPhoneNumber() uses SHA-256 (crypto.createHash) instead
of reversible hex encoding (Buffer.toString('hex'))
- S05 (Medium): DecisionEngine.evaluate() wraps evaluation in Promise.race
with configurable evaluationTimeout; returns fallback decision on timeout
- S06 (Medium): CarrierFactory.getAllCarriers() is now async and properly
awaits isHealthy() promises instead of returning raw Promise objects
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createHash } from 'crypto';
|
||||
import { DecisionResult } from '../engine/decision-engine';
|
||||
|
||||
export interface AlertEvent {
|
||||
@@ -29,14 +30,20 @@ export interface AlertServerConfig {
|
||||
heartbeatIntervalMs?: number;
|
||||
maxClients?: number;
|
||||
enableLogging?: boolean;
|
||||
enableAuth?: boolean;
|
||||
jwtSecret?: string;
|
||||
allowedOrigins?: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: Required<AlertServerConfig> = {
|
||||
port: 8080,
|
||||
host: '0.0.0.0',
|
||||
heartbeatIntervalMs: 30000,
|
||||
maxClients: 1000,
|
||||
maxClients: 100,
|
||||
enableLogging: true,
|
||||
enableAuth: true,
|
||||
jwtSecret: process.env.SPAMSHIELD_JWT_SECRET || '',
|
||||
allowedOrigins: ['http://localhost:3000'],
|
||||
};
|
||||
|
||||
export class AlertServer {
|
||||
@@ -57,9 +64,34 @@ export class AlertServer {
|
||||
}
|
||||
|
||||
private setupWebSocketHandlers(): void {
|
||||
this.wss.on('connection', (ws: WebSocket, req: any) => {
|
||||
this.wss.on('connection', async (ws: WebSocket, req: any) => {
|
||||
const origin = req.headers.origin;
|
||||
if (origin && this.config.allowedOrigins.length > 0 && !this.config.allowedOrigins.includes(origin)) {
|
||||
ws.close(1008, 'Origin not allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config.enableAuth && this.config.jwtSecret) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
ws.close(4001, 'Missing or invalid JWT token');
|
||||
return;
|
||||
}
|
||||
const token = authHeader.substring(7);
|
||||
const valid = await this.verifyJWT(token);
|
||||
if (!valid) {
|
||||
ws.close(4002, 'Invalid or expired JWT token');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clients.size >= this.config.maxClients) {
|
||||
ws.close(1013, 'Too many clients');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = req.headers['x-client-id'] as string || `client-${Date.now()}-${Math.random()}`;
|
||||
|
||||
|
||||
const subscription: ClientSubscription = {
|
||||
clientId,
|
||||
subscribedEvents: ['decision', 'flag', 'block', 'user_feedback', 'carrier_action'],
|
||||
@@ -281,6 +313,21 @@ export class AlertServer {
|
||||
}
|
||||
|
||||
private hashPhoneNumber(phoneNumber: string): string {
|
||||
return Buffer.from(phoneNumber).toString('hex');
|
||||
return createHash('sha256').update(phoneNumber).digest('hex');
|
||||
}
|
||||
|
||||
private async verifyJWT(token: string): Promise<boolean> {
|
||||
try {
|
||||
const { jwtVerify } = await import('jose');
|
||||
await jwtVerify(token, new TextEncoder().encode(this.config.jwtSecret), {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
if (this.config.enableLogging) {
|
||||
console.log('[AlertServer] JWT verification failed');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user