From 0fbcc240bee4c355fd93dfe3d35e05226b6495a6 Mon Sep 17 00:00:00 2001 From: lockbitchat Date: Sun, 17 May 2026 23:01:58 -0400 Subject: [PATCH] fix: add inbound message rate limiting --- package.json | 2 +- src/network/EnhancedSecureWebRTCManager.js | 62 ++++++++++++ tests/inbound-message-rate-limit.test.mjs | 106 +++++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/inbound-message-rate-limit.test.mjs diff --git a/package.json b/package.json index 72dbc23..f5d29f2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dev": "npm run build && python -m http.server 8000", "watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch", "serve": "npx http-server -p 8000", - "test": "node tests/sas-verification.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs" + "test": "node tests/sas-verification.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs" }, "keywords": [ "p2p", diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js index b7df56e..8de12c4 100644 --- a/src/network/EnhancedSecureWebRTCManager.js +++ b/src/network/EnhancedSecureWebRTCManager.js @@ -1486,6 +1486,47 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida return true; } + /** + * Dedicated receiver-side limiter. Keep separate from outbound quotas so a + * noisy peer cannot consume local send capacity or force decrypt/render work. + */ + _checkInboundRateLimit(context = 'incoming_message') { + const now = Date.now(); + + if (!this._inboundRateLimiter) { + this._inboundRateLimiter = { + messageCount: 0, + lastReset: now, + burstCount: 0, + lastBurstReset: now + }; + } + + if (now - this._inboundRateLimiter.lastReset > 60000) { + this._inboundRateLimiter.messageCount = 0; + this._inboundRateLimiter.lastReset = now; + } + + if (now - this._inboundRateLimiter.lastBurstReset > 1000) { + this._inboundRateLimiter.burstCount = 0; + this._inboundRateLimiter.lastBurstReset = now; + } + + if (this._inboundRateLimiter.burstCount >= this._inputValidationLimits.rateLimitBurstSize) { + this._secureLog('warn', '⚠️ Inbound message burst limit exceeded; dropping message', { context }); + return false; + } + + if (this._inboundRateLimiter.messageCount >= this._inputValidationLimits.rateLimitMessagesPerMinute) { + this._secureLog('warn', '⚠️ Inbound message rate limit exceeded; dropping message', { context }); + return false; + } + + this._inboundRateLimiter.messageCount++; + this._inboundRateLimiter.burstCount++; + return true; + } + // ============================================ // SECURE KEY STORAGE MANAGEMENT // ============================================ @@ -6416,6 +6457,9 @@ async processMessage(data) { if (parsed.type === 'enhanced_message') { this._secureLog('debug', '🔐 Enhanced message detected in processMessage'); + if (!this._checkInboundRateLimit('processMessage:enhanced_message')) { + return; + } try { // Decrypt enhanced message @@ -6458,6 +6502,9 @@ async processMessage(data) { if (parsed.type === 'message') { this._secureLog('debug', '📝 Regular user message detected in processMessage'); + if (!this._checkInboundRateLimit('processMessage:message')) { + return; + } if (this.onMessage && parsed.data) { this.deliverMessageToUI(parsed.data, 'received'); } @@ -6484,6 +6531,9 @@ async processMessage(data) { } catch (jsonError) { // Not JSON — treat as text WITHOUT mutex + if (!this._checkInboundRateLimit('processMessage:text')) { + return; + } if (this.onMessage) { this.deliverMessageToUI(data, 'received'); } @@ -7448,6 +7498,9 @@ async processMessage(data) { // ============================================ if (parsed.type === 'message' && parsed.data) { + if (!this._checkInboundRateLimit('dataChannel:message')) { + return; + } if (this.onMessage) { this.deliverMessageToUI(parsed.data, 'received'); } @@ -7466,6 +7519,9 @@ async processMessage(data) { } catch (jsonError) { // Not JSON — treat as regular text message + if (!this._checkInboundRateLimit('dataChannel:text')) { + return; + } if (this.onMessage) { this.deliverMessageToUI(event.data, 'received'); } @@ -7484,6 +7540,9 @@ async processMessage(data) { // FIX 4: New method for processing binary data WITHOUT mutex async _processBinaryDataWithoutMutex(data) { try { + if (!this._checkInboundRateLimit('binary_message')) { + return; + } // Apply security layers WITHOUT mutex let processedData = data; @@ -7546,6 +7605,9 @@ async processMessage(data) { // FIX 3: New method for processing enhanced messages WITHOUT mutex async _processEnhancedMessageWithoutMutex(parsedMessage) { try { + if (!this._checkInboundRateLimit('enhanced_message')) { + return; + } if (!this.encryptionKey || !this.macKey || !this.metadataKey) { this._secureLog('error', 'Missing encryption keys for enhanced message'); diff --git a/tests/inbound-message-rate-limit.test.mjs b/tests/inbound-message-rate-limit.test.mjs new file mode 100644 index 0000000..36b4b3b --- /dev/null +++ b/tests/inbound-message-rate-limit.test.mjs @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict'; + +globalThis.window = { + EnhancedSecureCryptoUtils: { + async decryptMessage() { + return { message: JSON.stringify({ type: 'message', data: 'enhanced hello' }) }; + } + } +}; + +const { EnhancedSecureWebRTCManager } = await import('../src/network/EnhancedSecureWebRTCManager.js'); + +function fakeManager({ perMinute = 60, burst = 10 } = {}) { + return { + delivered: [], + logs: [], + _inputValidationLimits: { + rateLimitMessagesPerMinute: perMinute, + rateLimitBurstSize: burst + }, + _checkInboundRateLimit: EnhancedSecureWebRTCManager.prototype._checkInboundRateLimit, + _secureLog(level, message, context) { + this.logs.push({ level, message, context }); + }, + onMessage() {}, + deliverMessageToUI(message, type) { + this.delivered.push({ message, type }); + } + }; +} + +// Normal inbound messages are delivered. +{ + const manager = fakeManager(); + await EnhancedSecureWebRTCManager.prototype.processMessage.call( + manager, + JSON.stringify({ type: 'message', data: 'hello' }) + ); + assert.deepEqual(manager.delivered, [{ message: 'hello', type: 'received' }]); +} + +// Burst floods are dropped safely and logged. +{ + const manager = fakeManager({ burst: 1 }); + await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'first' })); + await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'second' })); + assert.deepEqual(manager.delivered, [{ message: 'first', type: 'received' }]); + assert.match(manager.logs.at(-1).message, /Inbound message burst limit exceeded/); +} + +// Sustained-window floods are rejected independently of burst accounting. +{ + const manager = fakeManager({ perMinute: 1, burst: 10 }); + await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'first' })); + manager._inboundRateLimiter.lastBurstReset = Date.now() - 1001; + await EnhancedSecureWebRTCManager.prototype.processMessage.call(manager, JSON.stringify({ type: 'message', data: 'second' })); + assert.deepEqual(manager.delivered, [{ message: 'first', type: 'received' }]); + assert.match(manager.logs.at(-1).message, /Inbound message rate limit exceeded/); +} + +// Binary and enhanced helpers are guarded before expensive processing. +{ + const binaryManager = { + ...fakeManager({ burst: 0 }), + securityFeatures: { + hasNestedEncryption: false, + hasPacketPadding: false, + hasAntiFingerprinting: false + } + }; + await EnhancedSecureWebRTCManager.prototype._processBinaryDataWithoutMutex.call( + binaryManager, + new TextEncoder().encode('binary hello').buffer + ); + assert.deepEqual(binaryManager.delivered, []); + + const enhancedManager = { + ...fakeManager({ burst: 0 }), + encryptionKey: {}, + macKey: {}, + metadataKey: {} + }; + await EnhancedSecureWebRTCManager.prototype._processEnhancedMessageWithoutMutex.call( + enhancedManager, + { data: 'ciphertext' } + ); + assert.deepEqual(enhancedManager.delivered, []); +} + +// Outbound limiter remains a separate state machine. +{ + const manager = { + _inputValidationLimits: { + rateLimitMessagesPerMinute: 1, + rateLimitBurstSize: 1 + }, + _secureLog() {}, + _checkRateLimit: EnhancedSecureWebRTCManager.prototype._checkRateLimit, + _checkInboundRateLimit: EnhancedSecureWebRTCManager.prototype._checkInboundRateLimit + }; + assert.equal(manager._checkRateLimit('send'), true); + assert.equal(manager._checkInboundRateLimit('receive'), true); + assert.notEqual(manager._rateLimiter, manager._inboundRateLimiter); +} + +console.log('Inbound message rate limit tests passed');