feat(security): add security shield coordinator and middleware
Add main security shield that coordinates all security checks: - IP blocklist checking - Rate limiting (auth, connections, requests, webhooks, pairing) - Intrusion detection integration - Security event logging Add HTTP middleware for Express/HTTP integration: - Request rate limiting middleware - Connection rate limit checks - Auth rate limit checks - Webhook rate limit checks - Pairing rate limit checks Features: - Extract IP from X-Forwarded-For/X-Real-IP headers - Security context creation from requests - Unified API for all security checks Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6c6d11c354
commit
79597b7a98
180
src/security/middleware.ts
Normal file
180
src/security/middleware.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* Security shield HTTP middleware
|
||||||
|
* Integrates security checks into Express/HTTP request pipeline
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
import { getSecurityShield, SecurityShield, type SecurityContext } from "./shield.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create security context from HTTP request
|
||||||
|
*/
|
||||||
|
export function createSecurityContext(req: IncomingMessage): SecurityContext {
|
||||||
|
return {
|
||||||
|
ip: SecurityShield.extractIp(req),
|
||||||
|
userAgent: req.headers["user-agent"],
|
||||||
|
requestId: (req as any).requestId, // May be set by other middleware
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security middleware for HTTP requests
|
||||||
|
* Checks IP blocklist and rate limits
|
||||||
|
*/
|
||||||
|
export function securityMiddleware(
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
next: () => void
|
||||||
|
): void {
|
||||||
|
const shield = getSecurityShield();
|
||||||
|
|
||||||
|
if (!shield.isEnabled()) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = createSecurityContext(req);
|
||||||
|
|
||||||
|
// Check if IP is blocked
|
||||||
|
if (shield.isIpBlocked(ctx.ip)) {
|
||||||
|
res.statusCode = 403;
|
||||||
|
res.setHeader("Content-Type", "text/plain");
|
||||||
|
res.end("Forbidden: IP blocked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check request rate limit
|
||||||
|
const requestCheck = shield.checkRequest(ctx);
|
||||||
|
if (!requestCheck.allowed) {
|
||||||
|
res.statusCode = 429;
|
||||||
|
res.setHeader("Content-Type", "text/plain");
|
||||||
|
res.setHeader("Retry-After", String(Math.ceil((requestCheck.rateLimitInfo?.retryAfterMs ?? 60000) / 1000)));
|
||||||
|
res.end("Too Many Requests");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection rate limit check
|
||||||
|
* Call this when accepting new connections
|
||||||
|
*/
|
||||||
|
export function checkConnectionRateLimit(req: IncomingMessage): {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
} {
|
||||||
|
const shield = getSecurityShield();
|
||||||
|
|
||||||
|
if (!shield.isEnabled()) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = createSecurityContext(req);
|
||||||
|
const result = shield.checkConnection(ctx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: result.allowed,
|
||||||
|
reason: result.reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication rate limit check
|
||||||
|
* Call this before processing authentication
|
||||||
|
*/
|
||||||
|
export function checkAuthRateLimit(req: IncomingMessage, deviceId?: string): {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
retryAfterMs?: number;
|
||||||
|
} {
|
||||||
|
const shield = getSecurityShield();
|
||||||
|
|
||||||
|
if (!shield.isEnabled()) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = createSecurityContext(req);
|
||||||
|
if (deviceId) {
|
||||||
|
ctx.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = shield.checkAuthAttempt(ctx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: result.allowed,
|
||||||
|
reason: result.reason,
|
||||||
|
retryAfterMs: result.rateLimitInfo?.retryAfterMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log failed authentication
|
||||||
|
* Call this after authentication fails
|
||||||
|
*/
|
||||||
|
export function logAuthFailure(req: IncomingMessage, reason: string, deviceId?: string): void {
|
||||||
|
const shield = getSecurityShield();
|
||||||
|
|
||||||
|
if (!shield.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = createSecurityContext(req);
|
||||||
|
if (deviceId) {
|
||||||
|
ctx.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
shield.logAuthFailure(ctx, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pairing rate limit check
|
||||||
|
*/
|
||||||
|
export function checkPairingRateLimit(params: {
|
||||||
|
channel: string;
|
||||||
|
sender: string;
|
||||||
|
ip: string;
|
||||||
|
}): {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
} {
|
||||||
|
const shield = getSecurityShield();
|
||||||
|
|
||||||
|
if (!shield.isEnabled()) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = shield.checkPairingRequest(params);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: result.allowed,
|
||||||
|
reason: result.reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook rate limit check
|
||||||
|
*/
|
||||||
|
export function checkWebhookRateLimit(params: {
|
||||||
|
token: string;
|
||||||
|
path: string;
|
||||||
|
ip: string;
|
||||||
|
}): {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
retryAfterMs?: number;
|
||||||
|
} {
|
||||||
|
const shield = getSecurityShield();
|
||||||
|
|
||||||
|
if (!shield.isEnabled()) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = shield.checkWebhook(params);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: result.allowed,
|
||||||
|
reason: result.reason,
|
||||||
|
retryAfterMs: result.rateLimitInfo?.retryAfterMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
470
src/security/shield.ts
Normal file
470
src/security/shield.ts
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
/**
|
||||||
|
* Security shield coordinator
|
||||||
|
* Main entry point for all security checks
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IncomingMessage } from "node:http";
|
||||||
|
import { rateLimiter, RateLimitKeys, type RateLimit, type RateLimitResult } from "./rate-limiter.js";
|
||||||
|
import { ipManager } from "./ip-manager.js";
|
||||||
|
import { intrusionDetector } from "./intrusion-detector.js";
|
||||||
|
import { securityLogger } from "./events/logger.js";
|
||||||
|
import { SecurityActions } from "./events/schema.js";
|
||||||
|
import type { SecurityShieldConfig } from "../config/types.security.js";
|
||||||
|
import { DEFAULT_SECURITY_CONFIG } from "../config/types.security.js";
|
||||||
|
|
||||||
|
export interface SecurityContext {
|
||||||
|
ip: string;
|
||||||
|
deviceId?: string;
|
||||||
|
userId?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityCheckResult {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
rateLimitInfo?: RateLimitResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security shield - coordinates all security checks
|
||||||
|
*/
|
||||||
|
export class SecurityShield {
|
||||||
|
private config: Required<SecurityShieldConfig>;
|
||||||
|
|
||||||
|
constructor(config?: SecurityShieldConfig) {
|
||||||
|
this.config = {
|
||||||
|
enabled: config?.enabled ?? DEFAULT_SECURITY_CONFIG.shield.enabled,
|
||||||
|
rateLimiting: config?.rateLimiting ?? DEFAULT_SECURITY_CONFIG.shield.rateLimiting,
|
||||||
|
intrusionDetection: config?.intrusionDetection ?? DEFAULT_SECURITY_CONFIG.shield.intrusionDetection,
|
||||||
|
ipManagement: config?.ipManagement ?? DEFAULT_SECURITY_CONFIG.shield.ipManagement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if security shield is enabled
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return this.config.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP is blocked
|
||||||
|
*/
|
||||||
|
isIpBlocked(ip: string): boolean {
|
||||||
|
if (!this.config.enabled) return false;
|
||||||
|
return ipManager.isBlocked(ip) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check authentication attempt
|
||||||
|
*/
|
||||||
|
checkAuthAttempt(ctx: SecurityContext): SecurityCheckResult {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ip } = ctx;
|
||||||
|
|
||||||
|
// Check IP blocklist
|
||||||
|
const blockReason = ipManager.isBlocked(ip);
|
||||||
|
if (blockReason) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `IP blocked: ${blockReason}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit per-IP
|
||||||
|
if (this.config.rateLimiting?.enabled && this.config.rateLimiting.perIp?.authAttempts) {
|
||||||
|
const limit = this.config.rateLimiting.perIp.authAttempts;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.authAttempt(ip), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logRateLimit({
|
||||||
|
action: SecurityActions.RATE_LIMIT_EXCEEDED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: "auth",
|
||||||
|
details: {
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
retryAfterMs: result.retryAfterMs,
|
||||||
|
},
|
||||||
|
deviceId: ctx.deviceId,
|
||||||
|
requestId: ctx.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Rate limit exceeded",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit per-device (if deviceId provided)
|
||||||
|
if (ctx.deviceId && this.config.rateLimiting?.enabled && this.config.rateLimiting.perDevice?.authAttempts) {
|
||||||
|
const limit = this.config.rateLimiting.perDevice.authAttempts;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.authAttemptDevice(ctx.deviceId), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logRateLimit({
|
||||||
|
action: SecurityActions.RATE_LIMIT_EXCEEDED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: "auth",
|
||||||
|
details: {
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
retryAfterMs: result.retryAfterMs,
|
||||||
|
},
|
||||||
|
deviceId: ctx.deviceId,
|
||||||
|
requestId: ctx.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Rate limit exceeded (device)",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log failed authentication attempt
|
||||||
|
* Triggers intrusion detection for brute force
|
||||||
|
*/
|
||||||
|
logAuthFailure(ctx: SecurityContext, reason: string): void {
|
||||||
|
if (!this.config.enabled) return;
|
||||||
|
|
||||||
|
const { ip } = ctx;
|
||||||
|
|
||||||
|
// Log the failure
|
||||||
|
securityLogger.logAuth({
|
||||||
|
action: SecurityActions.AUTH_FAILED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: "gateway_auth",
|
||||||
|
details: { reason },
|
||||||
|
deviceId: ctx.deviceId,
|
||||||
|
userId: ctx.userId,
|
||||||
|
userAgent: ctx.userAgent,
|
||||||
|
requestId: ctx.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for brute force pattern
|
||||||
|
if (this.config.intrusionDetection?.enabled) {
|
||||||
|
const event = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
eventId: "",
|
||||||
|
severity: "warn" as const,
|
||||||
|
category: "authentication" as const,
|
||||||
|
ip,
|
||||||
|
action: SecurityActions.AUTH_FAILED,
|
||||||
|
resource: "gateway_auth",
|
||||||
|
outcome: "deny" as const,
|
||||||
|
details: { reason },
|
||||||
|
};
|
||||||
|
|
||||||
|
intrusionDetector.checkBruteForce({ ip, event });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check connection rate limit
|
||||||
|
*/
|
||||||
|
checkConnection(ctx: SecurityContext): SecurityCheckResult {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ip } = ctx;
|
||||||
|
|
||||||
|
// Check IP blocklist
|
||||||
|
const blockReason = ipManager.isBlocked(ip);
|
||||||
|
if (blockReason) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `IP blocked: ${blockReason}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit connections
|
||||||
|
if (this.config.rateLimiting?.enabled && this.config.rateLimiting.perIp?.connections) {
|
||||||
|
const limit = this.config.rateLimiting.perIp.connections;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.connection(ip), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logRateLimit({
|
||||||
|
action: SecurityActions.CONNECTION_LIMIT_EXCEEDED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: "gateway_connection",
|
||||||
|
details: {
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
},
|
||||||
|
requestId: ctx.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for port scanning
|
||||||
|
if (this.config.intrusionDetection?.enabled) {
|
||||||
|
const event = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
eventId: "",
|
||||||
|
severity: "warn" as const,
|
||||||
|
category: "network_access" as const,
|
||||||
|
ip,
|
||||||
|
action: SecurityActions.CONNECTION_LIMIT_EXCEEDED,
|
||||||
|
resource: "gateway_connection",
|
||||||
|
outcome: "deny" as const,
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
intrusionDetector.checkPortScanning({ ip, event });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Connection rate limit exceeded",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check request rate limit
|
||||||
|
*/
|
||||||
|
checkRequest(ctx: SecurityContext): SecurityCheckResult {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ip } = ctx;
|
||||||
|
|
||||||
|
// Check IP blocklist
|
||||||
|
const blockReason = ipManager.isBlocked(ip);
|
||||||
|
if (blockReason) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `IP blocked: ${blockReason}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit requests
|
||||||
|
if (this.config.rateLimiting?.enabled && this.config.rateLimiting.perIp?.requests) {
|
||||||
|
const limit = this.config.rateLimiting.perIp.requests;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.request(ip), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logRateLimit({
|
||||||
|
action: SecurityActions.RATE_LIMIT_EXCEEDED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: "gateway_request",
|
||||||
|
details: {
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
},
|
||||||
|
requestId: ctx.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Request rate limit exceeded",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check pairing request rate limit
|
||||||
|
*/
|
||||||
|
checkPairingRequest(params: {
|
||||||
|
channel: string;
|
||||||
|
sender: string;
|
||||||
|
ip: string;
|
||||||
|
}): SecurityCheckResult {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { channel, sender, ip } = params;
|
||||||
|
|
||||||
|
// Check IP blocklist
|
||||||
|
const blockReason = ipManager.isBlocked(ip);
|
||||||
|
if (blockReason) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `IP blocked: ${blockReason}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit pairing requests
|
||||||
|
if (this.config.rateLimiting?.enabled && this.config.rateLimiting.perSender?.pairingRequests) {
|
||||||
|
const limit = this.config.rateLimiting.perSender.pairingRequests;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.pairingRequest(channel, sender), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logPairing({
|
||||||
|
action: SecurityActions.PAIRING_RATE_LIMIT,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
details: {
|
||||||
|
channel,
|
||||||
|
sender,
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Pairing rate limit exceeded",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check webhook rate limit
|
||||||
|
*/
|
||||||
|
checkWebhook(params: {
|
||||||
|
token: string;
|
||||||
|
path: string;
|
||||||
|
ip: string;
|
||||||
|
}): SecurityCheckResult {
|
||||||
|
if (!this.config.enabled) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token, path, ip } = params;
|
||||||
|
|
||||||
|
// Check IP blocklist
|
||||||
|
const blockReason = ipManager.isBlocked(ip);
|
||||||
|
if (blockReason) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `IP blocked: ${blockReason}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit per token
|
||||||
|
if (this.config.rateLimiting?.enabled && this.config.rateLimiting.webhook?.perToken) {
|
||||||
|
const limit = this.config.rateLimiting.webhook.perToken;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.webhookToken(token), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logRateLimit({
|
||||||
|
action: SecurityActions.RATE_LIMIT_EXCEEDED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: `webhook:${path}`,
|
||||||
|
details: {
|
||||||
|
token: `${token.substring(0, 8)}...`,
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Webhook rate limit exceeded (token)",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit per path
|
||||||
|
if (this.config.rateLimiting?.enabled && this.config.rateLimiting.webhook?.perPath) {
|
||||||
|
const limit = this.config.rateLimiting.webhook.perPath;
|
||||||
|
const result = rateLimiter.check(RateLimitKeys.webhookPath(path), limit);
|
||||||
|
|
||||||
|
if (!result.allowed) {
|
||||||
|
securityLogger.logRateLimit({
|
||||||
|
action: SecurityActions.RATE_LIMIT_EXCEEDED,
|
||||||
|
ip,
|
||||||
|
outcome: "deny",
|
||||||
|
severity: "warn",
|
||||||
|
resource: `webhook:${path}`,
|
||||||
|
details: {
|
||||||
|
limit: limit.max,
|
||||||
|
windowMs: limit.windowMs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Webhook rate limit exceeded (path)",
|
||||||
|
rateLimitInfo: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract IP from request
|
||||||
|
*/
|
||||||
|
static extractIp(req: IncomingMessage): string {
|
||||||
|
// Try X-Forwarded-For first (if behind proxy)
|
||||||
|
const forwarded = req.headers["x-forwarded-for"];
|
||||||
|
if (forwarded) {
|
||||||
|
const ips = typeof forwarded === "string" ? forwarded.split(",") : forwarded;
|
||||||
|
const clientIp = ips[0]?.trim();
|
||||||
|
if (clientIp) return clientIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try X-Real-IP
|
||||||
|
const realIp = req.headers["x-real-ip"];
|
||||||
|
if (realIp && typeof realIp === "string") {
|
||||||
|
return realIp.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to socket remote address
|
||||||
|
return req.socket?.remoteAddress ?? "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton security shield instance
|
||||||
|
*/
|
||||||
|
export let securityShield: SecurityShield;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize security shield with config
|
||||||
|
*/
|
||||||
|
export function initSecurityShield(config?: SecurityShieldConfig): void {
|
||||||
|
securityShield = new SecurityShield(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get security shield instance (creates default if not initialized)
|
||||||
|
*/
|
||||||
|
export function getSecurityShield(): SecurityShield {
|
||||||
|
if (!securityShield) {
|
||||||
|
securityShield = new SecurityShield();
|
||||||
|
}
|
||||||
|
return securityShield;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user