fix(security): critical security and stability fixes
This commit is contained in:
@@ -35,7 +35,7 @@ export class HttpProxy implements IMetricsTracker {
|
||||
public routes: IRouteConfig[] = [];
|
||||
|
||||
// Server instances (HTTP/2 with HTTP/1 fallback)
|
||||
public httpsServer: any;
|
||||
public httpsServer: plugins.http2.Http2SecureServer;
|
||||
|
||||
// Core components
|
||||
private certificateManager: CertificateManager;
|
||||
@@ -196,8 +196,9 @@ export class HttpProxy implements IMetricsTracker {
|
||||
this.options.keepAliveTimeout = keepAliveTimeout;
|
||||
|
||||
if (this.httpsServer) {
|
||||
this.httpsServer.keepAliveTimeout = keepAliveTimeout;
|
||||
this.logger.info(`Updated keep-alive timeout to ${keepAliveTimeout}ms`);
|
||||
// HTTP/2 servers have setTimeout method for timeout management
|
||||
this.httpsServer.setTimeout(keepAliveTimeout);
|
||||
this.logger.info(`Updated server timeout to ${keepAliveTimeout}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,18 +250,19 @@ export class HttpProxy implements IMetricsTracker {
|
||||
this.setupConnectionTracking();
|
||||
|
||||
// Handle incoming HTTP/2 streams
|
||||
this.httpsServer.on('stream', (stream: any, headers: any) => {
|
||||
this.httpsServer.on('stream', (stream: plugins.http2.ServerHttp2Stream, headers: plugins.http2.IncomingHttpHeaders) => {
|
||||
this.requestHandler.handleHttp2(stream, headers);
|
||||
});
|
||||
// Handle HTTP/1.x fallback requests
|
||||
this.httpsServer.on('request', (req: any, res: any) => {
|
||||
this.httpsServer.on('request', (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => {
|
||||
this.requestHandler.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// Share server with certificate manager for dynamic contexts
|
||||
this.certificateManager.setHttpsServer(this.httpsServer);
|
||||
// Cast to https.Server as Http2SecureServer is compatible for certificate contexts
|
||||
this.certificateManager.setHttpsServer(this.httpsServer as any);
|
||||
// Setup WebSocket support on HTTP/1 fallback
|
||||
this.webSocketHandler.initialize(this.httpsServer);
|
||||
this.webSocketHandler.initialize(this.httpsServer as any);
|
||||
// Start metrics logging
|
||||
this.setupMetricsCollection();
|
||||
// Start periodic connection pool cleanup
|
||||
@@ -275,6 +277,21 @@ export class HttpProxy implements IMetricsTracker {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a loopback address (IPv4 or IPv6)
|
||||
*/
|
||||
private isLoopback(addr?: string): boolean {
|
||||
if (!addr) return false;
|
||||
// Check for IPv6 loopback
|
||||
if (addr === '::1') return true;
|
||||
// Handle IPv6-mapped IPv4 addresses
|
||||
if (addr.startsWith('::ffff:')) {
|
||||
addr = addr.substring(7);
|
||||
}
|
||||
// Check for IPv4 loopback range (127.0.0.0/8)
|
||||
return addr.startsWith('127.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up tracking of TCP connections
|
||||
*/
|
||||
@@ -282,30 +299,47 @@ export class HttpProxy implements IMetricsTracker {
|
||||
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
|
||||
let remoteIP = connection.remoteAddress || '';
|
||||
const connectionId = Math.random().toString(36).substring(2, 15);
|
||||
const isFromSmartProxy = this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1');
|
||||
const isFromSmartProxy = this.options.portProxyIntegration && this.isLoopback(connection.remoteAddress);
|
||||
|
||||
// For SmartProxy connections, wait for CLIENT_IP header
|
||||
if (isFromSmartProxy) {
|
||||
let headerBuffer = Buffer.alloc(0);
|
||||
let headerParsed = false;
|
||||
|
||||
const parseHeader = (data: Buffer) => {
|
||||
if (headerParsed) return data;
|
||||
const MAX_PREFACE = 256; // bytes - prevent DoS
|
||||
const HEADER_TIMEOUT_MS = 500; // timeout for header parsing
|
||||
let headerTimer: NodeJS.Timeout | undefined;
|
||||
let buffered = Buffer.alloc(0);
|
||||
|
||||
const onData = (chunk: Buffer) => {
|
||||
buffered = Buffer.concat([buffered, chunk]);
|
||||
|
||||
headerBuffer = Buffer.concat([headerBuffer, data]);
|
||||
const headerStr = headerBuffer.toString();
|
||||
const headerEnd = headerStr.indexOf('\r\n');
|
||||
// Prevent unbounded growth
|
||||
if (buffered.length > MAX_PREFACE) {
|
||||
connection.removeListener('data', onData);
|
||||
if (headerTimer) clearTimeout(headerTimer);
|
||||
this.logger.warn('Header preface too large, closing connection');
|
||||
connection.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (headerEnd !== -1) {
|
||||
const header = headerStr.substring(0, headerEnd);
|
||||
if (header.startsWith('CLIENT_IP:')) {
|
||||
remoteIP = header.substring(10); // Extract IP after "CLIENT_IP:"
|
||||
const idx = buffered.indexOf('\r\n');
|
||||
if (idx !== -1) {
|
||||
const headerLine = buffered.slice(0, idx).toString('utf8');
|
||||
if (headerLine.startsWith('CLIENT_IP:')) {
|
||||
remoteIP = headerLine.substring(10).trim();
|
||||
this.logger.debug(`Extracted client IP from SmartProxy: ${remoteIP}`);
|
||||
}
|
||||
headerParsed = true;
|
||||
|
||||
// Clean up listener and timer
|
||||
connection.removeListener('data', onData);
|
||||
if (headerTimer) clearTimeout(headerTimer);
|
||||
|
||||
// Put remaining data back onto the stream
|
||||
const remaining = buffered.slice(idx + 2);
|
||||
if (remaining.length > 0) {
|
||||
connection.unshift(remaining);
|
||||
}
|
||||
|
||||
// Store the real IP on the connection
|
||||
(connection as any)._realRemoteIP = remoteIP;
|
||||
connection._realRemoteIP = remoteIP;
|
||||
|
||||
// Validate the real IP
|
||||
const ipValidation = this.securityManager.validateIP(remoteIP);
|
||||
@@ -318,35 +352,26 @@ export class HttpProxy implements IMetricsTracker {
|
||||
remoteIP
|
||||
);
|
||||
connection.destroy();
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Track connection by real IP
|
||||
this.securityManager.trackConnectionByIP(remoteIP, connectionId);
|
||||
|
||||
// Return remaining data after header
|
||||
return headerBuffer.slice(headerEnd + 2);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Set timeout for header parsing
|
||||
headerTimer = setTimeout(() => {
|
||||
connection.removeListener('data', onData);
|
||||
this.logger.warn('Header parsing timeout, closing connection');
|
||||
connection.destroy();
|
||||
}, HEADER_TIMEOUT_MS);
|
||||
|
||||
// Override the first data handler to parse header
|
||||
const originalEmit = connection.emit;
|
||||
connection.emit = function(event: string, ...args: any[]) {
|
||||
if (event === 'data' && !headerParsed) {
|
||||
const remaining = parseHeader(args[0]);
|
||||
if (remaining && remaining.length > 0) {
|
||||
// Call original emit with remaining data
|
||||
return originalEmit.apply(connection, ['data', remaining]);
|
||||
} else if (headerParsed) {
|
||||
// Header parsed but no remaining data
|
||||
return true;
|
||||
}
|
||||
// Header not complete yet, suppress this data event
|
||||
return true;
|
||||
}
|
||||
return originalEmit.apply(connection, [event, ...args]);
|
||||
} as any;
|
||||
// Unref the timer so it doesn't keep the process alive
|
||||
if (headerTimer.unref) headerTimer.unref();
|
||||
|
||||
// Use prependListener to get data first
|
||||
connection.prependListener('data', onData);
|
||||
} else {
|
||||
// Direct connection - validate immediately
|
||||
const ipValidation = this.securityManager.validateIP(remoteIP);
|
||||
@@ -385,8 +410,8 @@ export class HttpProxy implements IMetricsTracker {
|
||||
}
|
||||
|
||||
// Add connection to tracking with metadata
|
||||
(connection as any)._connectionId = connectionId;
|
||||
(connection as any)._remoteIP = remoteIP;
|
||||
connection._connectionId = connectionId;
|
||||
connection._remoteIP = remoteIP;
|
||||
this.socketMap.add(connection);
|
||||
this.connectedClients = this.socketMap.getArray().length;
|
||||
|
||||
@@ -409,8 +434,8 @@ export class HttpProxy implements IMetricsTracker {
|
||||
this.connectedClients = this.socketMap.getArray().length;
|
||||
|
||||
// Remove IP tracking
|
||||
const connId = (connection as any)._connectionId;
|
||||
const connIP = (connection as any)._realRemoteIP || (connection as any)._remoteIP;
|
||||
const connId = connection._connectionId;
|
||||
const connIP = connection._realRemoteIP || connection._remoteIP;
|
||||
if (connId && connIP) {
|
||||
this.securityManager.removeConnectionByIP(connIP, connId);
|
||||
}
|
||||
|
Reference in New Issue
Block a user