fix: add inbound message rate limiting

This commit is contained in:
lockbitchat
2026-05-17 23:01:58 -04:00
parent 18022c6b68
commit 0fbcc240be
3 changed files with 169 additions and 1 deletions
+1 -1
View File
@@ -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",
@@ -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');
+106
View File
@@ -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');