fix: add inbound message rate limiting
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"dev": "npm run build && python -m http.server 8000",
|
"dev": "npm run build && python -m http.server 8000",
|
||||||
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
||||||
"serve": "npx http-server -p 8000",
|
"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": [
|
"keywords": [
|
||||||
"p2p",
|
"p2p",
|
||||||
|
|||||||
@@ -1486,6 +1486,47 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
|
|||||||
return true;
|
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
|
// SECURE KEY STORAGE MANAGEMENT
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -6416,6 +6457,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
if (parsed.type === 'enhanced_message') {
|
if (parsed.type === 'enhanced_message') {
|
||||||
this._secureLog('debug', '🔐 Enhanced message detected in processMessage');
|
this._secureLog('debug', '🔐 Enhanced message detected in processMessage');
|
||||||
|
if (!this._checkInboundRateLimit('processMessage:enhanced_message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decrypt enhanced message
|
// Decrypt enhanced message
|
||||||
@@ -6458,6 +6502,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
if (parsed.type === 'message') {
|
if (parsed.type === 'message') {
|
||||||
this._secureLog('debug', '📝 Regular user message detected in processMessage');
|
this._secureLog('debug', '📝 Regular user message detected in processMessage');
|
||||||
|
if (!this._checkInboundRateLimit('processMessage:message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage && parsed.data) {
|
if (this.onMessage && parsed.data) {
|
||||||
this.deliverMessageToUI(parsed.data, 'received');
|
this.deliverMessageToUI(parsed.data, 'received');
|
||||||
}
|
}
|
||||||
@@ -6484,6 +6531,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
// Not JSON — treat as text WITHOUT mutex
|
// Not JSON — treat as text WITHOUT mutex
|
||||||
|
if (!this._checkInboundRateLimit('processMessage:text')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage) {
|
if (this.onMessage) {
|
||||||
this.deliverMessageToUI(data, 'received');
|
this.deliverMessageToUI(data, 'received');
|
||||||
}
|
}
|
||||||
@@ -7448,6 +7498,9 @@ async processMessage(data) {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
if (parsed.type === 'message' && parsed.data) {
|
if (parsed.type === 'message' && parsed.data) {
|
||||||
|
if (!this._checkInboundRateLimit('dataChannel:message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage) {
|
if (this.onMessage) {
|
||||||
this.deliverMessageToUI(parsed.data, 'received');
|
this.deliverMessageToUI(parsed.data, 'received');
|
||||||
}
|
}
|
||||||
@@ -7466,6 +7519,9 @@ async processMessage(data) {
|
|||||||
|
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
// Not JSON — treat as regular text message
|
// Not JSON — treat as regular text message
|
||||||
|
if (!this._checkInboundRateLimit('dataChannel:text')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.onMessage) {
|
if (this.onMessage) {
|
||||||
this.deliverMessageToUI(event.data, 'received');
|
this.deliverMessageToUI(event.data, 'received');
|
||||||
}
|
}
|
||||||
@@ -7484,6 +7540,9 @@ async processMessage(data) {
|
|||||||
// FIX 4: New method for processing binary data WITHOUT mutex
|
// FIX 4: New method for processing binary data WITHOUT mutex
|
||||||
async _processBinaryDataWithoutMutex(data) {
|
async _processBinaryDataWithoutMutex(data) {
|
||||||
try {
|
try {
|
||||||
|
if (!this._checkInboundRateLimit('binary_message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply security layers WITHOUT mutex
|
// Apply security layers WITHOUT mutex
|
||||||
let processedData = data;
|
let processedData = data;
|
||||||
@@ -7546,6 +7605,9 @@ async processMessage(data) {
|
|||||||
// FIX 3: New method for processing enhanced messages WITHOUT mutex
|
// FIX 3: New method for processing enhanced messages WITHOUT mutex
|
||||||
async _processEnhancedMessageWithoutMutex(parsedMessage) {
|
async _processEnhancedMessageWithoutMutex(parsedMessage) {
|
||||||
try {
|
try {
|
||||||
|
if (!this._checkInboundRateLimit('enhanced_message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.encryptionKey || !this.macKey || !this.metadataKey) {
|
if (!this.encryptionKey || !this.macKey || !this.metadataKey) {
|
||||||
this._secureLog('error', 'Missing encryption keys for enhanced message');
|
this._secureLog('error', 'Missing encryption keys for enhanced message');
|
||||||
|
|||||||
@@ -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');
|
||||||
Reference in New Issue
Block a user