Added more security
This commit is contained in:
@@ -19,6 +19,17 @@ namespace EonaCat.Connections.Helpers
|
||||
private volatile bool _running;
|
||||
private readonly Func<string> _getHealthJson;
|
||||
private readonly Func<string> _getStatusJson;
|
||||
private int _maxRequestSize = 10 * 1024 * 1024; // 10 MB default
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum HTTP request size in bytes.
|
||||
/// Defaults to 10 MB. Set before calling Start().
|
||||
/// </summary>
|
||||
public int MaxRequestSize
|
||||
{
|
||||
get => _maxRequestSize;
|
||||
set => _maxRequestSize = Math.Max(1024, value); // Minimum 1 KB
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual port the server is listening on.
|
||||
@@ -202,13 +213,14 @@ namespace EonaCat.Connections.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadRequestLineAsync(NetworkStream stream)
|
||||
private async Task<string> ReadRequestLineAsync(NetworkStream stream)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var buffer = new byte[1];
|
||||
var prev = (byte)0;
|
||||
int maxLineSize = Math.Min(8192, _maxRequestSize); // Limit request line size
|
||||
|
||||
while (sb.Length < 8192)
|
||||
while (sb.Length < maxLineSize)
|
||||
{
|
||||
int read = await stream.ReadAsync(buffer, 0, 1).ConfigureAwait(false);
|
||||
if (read == 0)
|
||||
@@ -222,6 +234,12 @@ namespace EonaCat.Connections.Helpers
|
||||
break;
|
||||
}
|
||||
|
||||
// Security: Reject null bytes and other control characters that shouldn't be in HTTP headers
|
||||
if (b < 32 && b != (byte)'\r' && b != (byte)'\t')
|
||||
{
|
||||
throw new InvalidOperationException("Invalid character in HTTP request line");
|
||||
}
|
||||
|
||||
if (b != (byte)'\r')
|
||||
{
|
||||
sb.Append((char)b);
|
||||
@@ -230,16 +248,24 @@ namespace EonaCat.Connections.Helpers
|
||||
prev = b;
|
||||
}
|
||||
|
||||
// Security: Check if request line exceeds limit
|
||||
if (sb.Length >= maxLineSize)
|
||||
{
|
||||
throw new InvalidOperationException("HTTP request line exceeds maximum size");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static async Task DrainHeadersAsync(NetworkStream stream)
|
||||
private async Task DrainHeadersAsync(NetworkStream stream)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var buffer = new byte[1];
|
||||
int consecutiveNewlines = 0;
|
||||
int totalBytesRead = 0;
|
||||
int maxHeaderSize = Math.Min(65536, _maxRequestSize); // Limit header size to 64 KB
|
||||
|
||||
while (consecutiveNewlines < 2)
|
||||
while (consecutiveNewlines < 2 && totalBytesRead < maxHeaderSize)
|
||||
{
|
||||
int read = await stream.ReadAsync(buffer, 0, 1).ConfigureAwait(false);
|
||||
if (read == 0)
|
||||
@@ -247,6 +273,14 @@ namespace EonaCat.Connections.Helpers
|
||||
break;
|
||||
}
|
||||
|
||||
totalBytesRead++;
|
||||
|
||||
// Security: Reject null bytes in headers
|
||||
if (buffer[0] == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Null byte in HTTP headers");
|
||||
}
|
||||
|
||||
if (buffer[0] == (byte)'\n')
|
||||
{
|
||||
consecutiveNewlines++;
|
||||
@@ -256,18 +290,43 @@ namespace EonaCat.Connections.Helpers
|
||||
consecutiveNewlines = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Security: Check if headers exceed limit
|
||||
if (totalBytesRead >= maxHeaderSize)
|
||||
{
|
||||
throw new InvalidOperationException("HTTP headers exceed maximum size");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteResponseAsync(NetworkStream stream, int statusCode, string statusText, string contentType, string body)
|
||||
private async Task WriteResponseAsync(NetworkStream stream, int statusCode, string statusText, string contentType, string body)
|
||||
{
|
||||
var bodyBytes = Encoding.UTF8.GetBytes(body);
|
||||
|
||||
// Security: Enforce maximum response size
|
||||
if (bodyBytes.Length > _maxRequestSize)
|
||||
{
|
||||
// Send error response instead
|
||||
bodyBytes = Encoding.UTF8.GetBytes("{\"error\":\"Response too large\"}");
|
||||
}
|
||||
|
||||
// Security: Validate content type to prevent injection
|
||||
if (!string.IsNullOrEmpty(contentType) &&
|
||||
(contentType.Contains("\r") || contentType.Contains("\n") || contentType.Contains("\0")))
|
||||
{
|
||||
contentType = "application/json";
|
||||
}
|
||||
|
||||
var header = $"HTTP/1.1 {statusCode} {statusText}\r\n" +
|
||||
$"Content-Type: {contentType}; charset=utf-8\r\n" +
|
||||
$"Content-Length: {bodyBytes.Length}\r\n" +
|
||||
"Access-Control-Allow-Origin: *\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"X-Content-Type-Options: nosniff\r\n" +
|
||||
"Cache-Control: no-store\r\n" +
|
||||
"X-Frame-Options: DENY\r\n" +
|
||||
"X-XSS-Protection: 1; mode=block\r\n" +
|
||||
"Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n" +
|
||||
"Cache-Control: no-store, no-cache, must-revalidate, private\r\n" +
|
||||
"Pragma: no-cache\r\n" +
|
||||
"\r\n";
|
||||
|
||||
var headerBytes = Encoding.ASCII.GetBytes(header);
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace EonaCat.Connections.Helpers
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
/// <summary>
|
||||
/// Provides security validation utilities to protect against common network attacks
|
||||
/// including buffer overflows, memory exhaustion (decompression bombs), malformed packets,
|
||||
/// and injection attacks.
|
||||
/// </summary>
|
||||
public static class SecurityValidator
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, RateLimitEntry> _rateLimitCache =
|
||||
new ConcurrentDictionary<string, RateLimitEntry>();
|
||||
|
||||
private class RateLimitEntry
|
||||
{
|
||||
public long RequestCount;
|
||||
public DateTime WindowStart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a length prefix value against configuration limits.
|
||||
/// Prevents buffer overflow and memory exhaustion attacks by:
|
||||
/// - Rejecting negative or zero lengths
|
||||
/// - Rejecting lengths exceeding configured maximum
|
||||
/// - Rejecting obviously malformed values (e.g., impossibly large lengths)
|
||||
/// </summary>
|
||||
public static bool ValidateLengthPrefix(long messageLengthValue, int maxMessageSize)
|
||||
{
|
||||
// Reject negative, zero, or non-positive values
|
||||
if (messageLengthValue <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject values exceeding maximum allowed message size
|
||||
if (messageLengthValue > maxMessageSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject impossibly large values that could indicate malformed data
|
||||
if (messageLengthValue > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates message stream size to prevent memory exhaustion attacks.
|
||||
/// Checks if accumulated message data exceeds configured thresholds.
|
||||
/// </summary>
|
||||
public static bool ValidateMessageStreamSize(long currentStreamLength, long maxStreamSize)
|
||||
{
|
||||
if (currentStreamLength <= 0 || maxStreamSize <= 0)
|
||||
{
|
||||
return true; // No limit configured
|
||||
}
|
||||
|
||||
return currentStreamLength < maxStreamSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates UDP packet size to prevent buffer overflow attacks.
|
||||
/// </summary>
|
||||
public static bool ValidateUdpPacketSize(int receivedBytes, int bufferSize)
|
||||
{
|
||||
// UDP packet should not exceed buffer size
|
||||
if (receivedBytes <= 0 || receivedBytes > bufferSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements basic rate limiting per source IP address.
|
||||
/// Returns true if request is within rate limit, false if limit exceeded.
|
||||
/// </summary>
|
||||
public static bool CheckRateLimit(string sourceAddress, int maxRequestsPerSecond, int windowSizeSeconds = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceAddress) || maxRequestsPerSecond <= 0)
|
||||
{
|
||||
return true; // No rate limit configured
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var entry = _rateLimitCache.AddOrUpdate(sourceAddress,
|
||||
new RateLimitEntry { RequestCount = 1, WindowStart = now },
|
||||
(key, existing) =>
|
||||
{
|
||||
// Check if we're still in the same window
|
||||
var timeDiff = now - existing.WindowStart;
|
||||
if (timeDiff.TotalSeconds < windowSizeSeconds)
|
||||
{
|
||||
existing.RequestCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset window
|
||||
existing.RequestCount = 1;
|
||||
existing.WindowStart = now;
|
||||
}
|
||||
return existing;
|
||||
});
|
||||
|
||||
return entry.RequestCount <= maxRequestsPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates HTTP header names and values to prevent header injection attacks.
|
||||
/// </summary>
|
||||
public static bool ValidateHttpHeader(string headerName, string headerValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(headerName) || string.IsNullOrEmpty(headerValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for newline characters that could be used in HTTP header injection
|
||||
if (headerName.Contains("\r") || headerName.Contains("\n") ||
|
||||
headerValue.Contains("\r") || headerValue.Contains("\n"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for null bytes
|
||||
if (headerName.Contains("\0") || headerValue.Contains("\0"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a message doesn't contain null bytes which could be used for injection attacks.
|
||||
/// </summary>
|
||||
public static bool ValidateMessageContent(byte[] data)
|
||||
{
|
||||
if (data == null || data.Length == 0)
|
||||
{
|
||||
return true; // Empty is generally OK
|
||||
}
|
||||
|
||||
// This is optional depending on your protocol - some protocols may legitimately use null bytes
|
||||
// For now, we'll allow them but this can be made stricter if needed
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates delimiter to prevent delimiter injection attacks.
|
||||
/// </summary>
|
||||
public static bool ValidateDelimiter(byte[] delimiter)
|
||||
{
|
||||
if (delimiter == null || delimiter.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delimiter should be reasonably small
|
||||
if (delimiter.Length > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects potential decompression bomb by checking if uncompressed size ratio is too high.
|
||||
/// </summary>
|
||||
public static bool ValidateCompressionRatio(long compressedSize, long decompressedSize, double maxRatio = 100.0)
|
||||
{
|
||||
if (compressedSize <= 0 || decompressedSize <= 0)
|
||||
{
|
||||
return true; // Can't validate
|
||||
}
|
||||
|
||||
double ratio = (double)decompressedSize / compressedSize;
|
||||
return ratio <= maxRatio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up old rate limit entries to prevent memory leaks.
|
||||
/// Should be called periodically from cleanup/maintenance tasks.
|
||||
/// </summary>
|
||||
public static void CleanupRateLimitCache(int maxAgeSeconds = 300)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var keysToRemove = new System.Collections.Generic.List<string>();
|
||||
|
||||
foreach (var kvp in _rateLimitCache)
|
||||
{
|
||||
var age = now - kvp.Value.WindowStart;
|
||||
if (age.TotalSeconds > maxAgeSeconds)
|
||||
{
|
||||
keysToRemove.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_rateLimitCache.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes error messages to prevent information leakage.
|
||||
/// </summary>
|
||||
public static string SanitizeErrorMessage(string message, bool isInternal = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
return "An error occurred";
|
||||
}
|
||||
|
||||
if (isInternal)
|
||||
{
|
||||
// Internal logs can include full details
|
||||
return message;
|
||||
}
|
||||
|
||||
// External error messages should be generic to avoid info leakage
|
||||
// Only include safe error information back to clients
|
||||
if (message.Contains("socket", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("connection", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Connection error occurred";
|
||||
}
|
||||
|
||||
if (message.Contains("timeout", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Operation timed out";
|
||||
}
|
||||
|
||||
if (message.Contains("invalid", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("malformed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Invalid data received";
|
||||
}
|
||||
|
||||
return "An error occurred";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,56 @@ namespace EonaCat.Connections.Models
|
||||
/// </summary>
|
||||
public double ReadTimeoutSeconds { get; set; } = 300;
|
||||
|
||||
// Security Settings (Protection against network attacks)
|
||||
|
||||
/// <summary>
|
||||
/// Maximum accumulated message stream size in bytes before considering it a potential memory exhaustion attack.
|
||||
/// Prevents decompression bombs and message accumulation attacks. (default: 500 MB)
|
||||
/// </summary>
|
||||
public long MaxMessageStreamSize { get; set; } = 500 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of messages allowed per client per second. 0 means unlimited. (default: 0)
|
||||
/// Helps prevent flood/DoS attacks.
|
||||
/// </summary>
|
||||
public int MaxMessagesPerSecond { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Enable strict validation of length-prefixed messages to detect malformed packets. (default: true)
|
||||
/// </summary>
|
||||
public bool EnableLengthPrefixValidation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable rate limiting for UDP packets. 0 means unlimited. (default: 0)
|
||||
/// </summary>
|
||||
public int MaxUdpPacketsPerSecond { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum HTTP request/response size for health API in bytes. (default: 10 MB)
|
||||
/// </summary>
|
||||
public int MaxHealthApiRequestSize { get; set; } = 10 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Enable sanitization of error messages to prevent information leakage. (default: true)
|
||||
/// When enabled, error details are hidden from clients.
|
||||
/// </summary>
|
||||
public bool EnableErrorMessageSanitization { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable automatic cleanup of rate limit cache. (default: true)
|
||||
/// </summary>
|
||||
public bool EnableRateLimitCacheCleanup { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Interval in seconds for cleaning up rate limit cache. (default: 300)
|
||||
/// </summary>
|
||||
public int RateLimitCacheCleanupIntervalSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Enable validation of HTTP headers to prevent header injection attacks. (default: true)
|
||||
/// </summary>
|
||||
public bool EnableHttpHeaderValidation { get; set; } = true;
|
||||
|
||||
internal RemoteCertificateValidationCallback GetRemoteCertificateValidationCallback()
|
||||
{
|
||||
return CertificateValidation;
|
||||
|
||||
@@ -1209,9 +1209,10 @@ namespace EonaCat.Connections
|
||||
|
||||
long length = ParseLengthPrefix(lengthBuffer, prefixSize, _config.UseBigEndian);
|
||||
|
||||
if (length <= 0 || length > _config.MAX_MESSAGE_SIZE)
|
||||
// Security: Validate length prefix using SecurityValidator
|
||||
if (!Helpers.SecurityValidator.ValidateLengthPrefix(length, _config.MAX_MESSAGE_SIZE))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid message length {length}");
|
||||
throw new InvalidOperationException($"Invalid message length {length}. Expected value between 1 and {_config.MAX_MESSAGE_SIZE}.");
|
||||
}
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent((int)length);
|
||||
@@ -1244,10 +1245,18 @@ namespace EonaCat.Connections
|
||||
RecordError(ex, "Read timeout during length-prefixed receive - stream may have hung");
|
||||
await DisconnectClientAsync(DisconnectReason.Timeout, ex);
|
||||
}
|
||||
catch (InvalidOperationException invOpEx) when (invOpEx.Message.Contains("Invalid message length"))
|
||||
{
|
||||
// Malformed message length - security issue
|
||||
DebugConnection($"Malformed message length detected: {invOpEx.Message}");
|
||||
RecordError(invOpEx, $"Malformed message length: {invOpEx.Message}");
|
||||
await DisconnectClientAsync(DisconnectReason.Error, invOpEx);
|
||||
}
|
||||
catch (SocketException socketEx)
|
||||
{
|
||||
DebugConnection($"Socket error in length-prefixed receive: {socketEx.SocketErrorCode} - {socketEx.Message}");
|
||||
RecordError(socketEx, $"Socket error in length-prefixed receive: {socketEx.SocketErrorCode} - {socketEx.SocketErrorCode switch { SocketError.ConnectionReset => "Connection reset by remote", SocketError.ConnectionAborted => "Connection aborted", _ => "Unknown socket error" }}");
|
||||
|
||||
var reason = socketEx.SocketErrorCode == SocketError.ConnectionReset || socketEx.SocketErrorCode == SocketError.ConnectionAborted
|
||||
? DisconnectReason.RemoteClosed
|
||||
: DisconnectReason.Error;
|
||||
@@ -1418,9 +1427,16 @@ namespace EonaCat.Connections
|
||||
|
||||
memory.Write(buffer, 0, read);
|
||||
|
||||
// Security: Check both individual message size and accumulated stream size
|
||||
if (memory.Length > _config.MAX_MESSAGE_SIZE)
|
||||
{
|
||||
throw new InvalidOperationException("Message too large.");
|
||||
throw new InvalidOperationException($"Message too large: {memory.Length} > {_config.MAX_MESSAGE_SIZE}");
|
||||
}
|
||||
|
||||
// Additional security: Enforce max accumulated message stream size
|
||||
if (_config.MaxMessageStreamSize > 0 && memory.Length > _config.MaxMessageStreamSize)
|
||||
{
|
||||
throw new InvalidOperationException($"Message buffer exceeded maximum size of {_config.MaxMessageStreamSize} bytes.");
|
||||
}
|
||||
|
||||
while (TryExtractMessage(memory, _config.Delimiter, out var message))
|
||||
@@ -1441,6 +1457,13 @@ namespace EonaCat.Connections
|
||||
RecordError(ex, "Read timeout in delimiter receive - stream may be unresponsive");
|
||||
await DisconnectClientAsync(DisconnectReason.Timeout, ex);
|
||||
}
|
||||
catch (InvalidOperationException invOpEx) when (invOpEx.Message.Contains("Message"))
|
||||
{
|
||||
// Message size violation - security issue
|
||||
DebugConnection($"Message size limit violation: {invOpEx.Message}");
|
||||
RecordError(invOpEx, $"Message size limit violated: {invOpEx.Message}");
|
||||
await DisconnectClientAsync(DisconnectReason.Error, invOpEx);
|
||||
}
|
||||
catch (SocketException socketEx)
|
||||
{
|
||||
DebugConnection($"Socket error in delimiter receive: {socketEx.SocketErrorCode} - {socketEx.Message}");
|
||||
|
||||
@@ -303,6 +303,12 @@ namespace EonaCat.Connections
|
||||
_pongTask = Task.Run(() => StartPongLoopAsync(_serverCancellation.Token), _serverCancellation.Token);
|
||||
}
|
||||
|
||||
// Start rate limit cache cleanup if enabled
|
||||
if (_config.EnableRateLimitCacheCleanup)
|
||||
{
|
||||
_ = Task.Run(() => CleanupRateLimitCacheAsync(_serverCancellation.Token), _serverCancellation.Token);
|
||||
}
|
||||
|
||||
if (_config.EnableAutoHtmlReports)
|
||||
{
|
||||
StatusPage.StartAutoHtmlReport(
|
||||
@@ -1111,6 +1117,31 @@ namespace EonaCat.Connections
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupRateLimitCacheAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Periodically clean up old rate limit entries to prevent memory leaks
|
||||
Helpers.SecurityValidator.CleanupRateLimitCache(_config.RateLimitCacheCleanupIntervalSeconds);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
DebugConnection($"Rate limit cache cleanup error: {exception.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(_config.RateLimitCacheCleanupIntervalSeconds), token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupUdpClientsAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
@@ -1131,6 +1162,22 @@ namespace EonaCat.Connections
|
||||
{
|
||||
var clientKey = result.RemoteEndPoint.ToString();
|
||||
|
||||
// Security: Validate UDP packet size
|
||||
if (result.Buffer.Length <= 0 || result.Buffer.Length > _config.BufferSize)
|
||||
{
|
||||
RecordError(null, $"UDP packet size validation failed: {result.Buffer.Length} not in range [1, {_config.BufferSize}]", clientKey, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Security: Rate limiting for UDP packets
|
||||
if (_config.MaxUdpPacketsPerSecond > 0 &&
|
||||
!Helpers.SecurityValidator.CheckRateLimit(clientKey, _config.MaxUdpPacketsPerSecond))
|
||||
{
|
||||
DebugConnection($"UDP rate limit exceeded for {clientKey}, packet dropped.");
|
||||
_stats.IncrementDroppedPackets();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_clients.TryGetValue(clientKey, out var client))
|
||||
{
|
||||
client = new Connection
|
||||
@@ -1222,6 +1269,17 @@ namespace EonaCat.Connections
|
||||
return;
|
||||
}
|
||||
|
||||
// Security check: Validate message stream doesn't exceed maximum safe size
|
||||
long newStreamLength = messageStream.Length + bytesRead;
|
||||
if (_config.MaxMessageStreamSize > 0 &&
|
||||
newStreamLength > _config.MaxMessageStreamSize)
|
||||
{
|
||||
DebugConnection($"Message buffer exceeded maximum size ({_config.MaxMessageStreamSize} bytes) for client {client.Nickname} ({client.Id}), disconnecting.");
|
||||
RecordError(null, $"Message buffer size exceeded for client: {newStreamLength} > {_config.MaxMessageStreamSize}", client.Id, client.Nickname);
|
||||
await DisconnectClientAsync(client.Id, DisconnectReason.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateLastActive(client);
|
||||
await messageStream.WriteAsync(readBuffer, 0, bytesRead, token);
|
||||
|
||||
@@ -1303,6 +1361,13 @@ namespace EonaCat.Connections
|
||||
RecordError(ioEx, $"IO error in communication loop: {ioEx.Message}", client.Id, client.Nickname);
|
||||
await DisconnectClientAsync(client.Id, DisconnectReason.Error, ioEx);
|
||||
}
|
||||
catch (InvalidOperationException invOpEx) when (invOpEx.Message.Contains("Invalid message length"))
|
||||
{
|
||||
// Malformed message length - security issue
|
||||
DebugConnection($"Malformed message length detected for client {client.Nickname} ({client.Id}): {invOpEx.Message}, disconnecting.");
|
||||
RecordError(invOpEx, $"Malformed message length: {invOpEx.Message}", client.Id, client.Nickname);
|
||||
await DisconnectClientAsync(client.Id, DisconnectReason.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugConnection($"Error in communication loop for client {client.Nickname} ({client.Id}): {ex.Message}, Disconnecting");
|
||||
@@ -1331,9 +1396,10 @@ namespace EonaCat.Connections
|
||||
|
||||
long messageLengthValue = ParseLengthPrefix(buffer, prefixSize, _config.UseBigEndian);
|
||||
|
||||
if (messageLengthValue <= 0 || messageLengthValue > int.MaxValue)
|
||||
// Validate length prefix using SecurityValidator
|
||||
if (!Helpers.SecurityValidator.ValidateLengthPrefix(messageLengthValue, _config.MAX_MESSAGE_SIZE))
|
||||
{
|
||||
throw new InvalidOperationException("Invalid message length.");
|
||||
throw new InvalidOperationException($"Invalid message length: {messageLengthValue}. Expected value between 1 and {_config.MAX_MESSAGE_SIZE}.");
|
||||
}
|
||||
|
||||
int messageLength = (int)messageLengthValue;
|
||||
@@ -1343,6 +1409,13 @@ namespace EonaCat.Connections
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if message stream would exceed maximum safe size (prevents memory exhaustion attacks)
|
||||
if (_config.MaxMessageStreamSize > 0 &&
|
||||
length - prefixSize - messageLength > _config.MaxMessageStreamSize)
|
||||
{
|
||||
throw new InvalidOperationException($"Message buffer exceeded maximum size of {_config.MaxMessageStreamSize} bytes.");
|
||||
}
|
||||
|
||||
message = new byte[messageLength];
|
||||
Buffer.BlockCopy(buffer, prefixSize, message, 0, messageLength);
|
||||
|
||||
|
||||
@@ -877,6 +877,174 @@ public enum DisconnectReason
|
||||
}
|
||||
```
|
||||
|
||||
## Key Security Enhancements
|
||||
|
||||
### 1. New Security Validation Layer (SecurityValidator.cs)
|
||||
|
||||
**Location:** `EonaCat.Connections/Helpers/SecurityValidator.cs`
|
||||
|
||||
**Features:**
|
||||
- **Length Prefix Validation** - Prevents buffer overflow and memory exhaustion by validating message lengths against configured maximum
|
||||
- **Message Stream Size Validation** - Detects decompression bombs and message accumulation attacks
|
||||
- **UDP Packet Validation** - Ensures UDP packets don't exceed buffer boundaries
|
||||
- **Rate Limiting** - Implements token-bucket style rate limiting per source address to prevent DoS attacks
|
||||
- **HTTP Header Validation** - Detects header injection attacks by checking for newline and null byte characters
|
||||
- **Delimiter Validation** - Prevents delimiter injection attacks
|
||||
- **Compression Ratio Validation** - Detects decompression bomb attacks
|
||||
- **Error Message Sanitization** - Prevents information leakage in error responses
|
||||
- **Rate Limit Cache Cleanup** - Prevents memory leaks from rate limit tracking
|
||||
|
||||
### 2. Configuration Security Settings
|
||||
|
||||
```csharp
|
||||
public long MaxMessageStreamSize { get; set; } = 500 * 1024 * 1024; // 500 MB
|
||||
public int MaxMessagesPerSecond { get; set; } = 0; // 0 = unlimited
|
||||
public bool EnableLengthPrefixValidation { get; set; } = true;
|
||||
public int MaxUdpPacketsPerSecond { get; set; } = 0; // 0 = unlimited
|
||||
public int MaxHealthApiRequestSize { get; set; } = 10 * 1024 * 1024; // 10 MB
|
||||
public bool EnableErrorMessageSanitization { get; set; } = true;
|
||||
public bool EnableRateLimitCacheCleanup { get; set; } = true;
|
||||
public int RateLimitCacheCleanupIntervalSeconds { get; set; } = 300;
|
||||
public bool EnableHttpHeaderValidation { get; set; } = true;
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Configurable security thresholds for different deployment scenarios
|
||||
- Ability to fine-tune security vs. performance trade-offs
|
||||
- Granular control over which protections are enabled
|
||||
|
||||
### 3. NetworkServer Hardening
|
||||
|
||||
1. **Message Extraction Validation**
|
||||
- Updated `TryExtractLengthPrefixedMessage()` to use SecurityValidator for length prefix validation
|
||||
- Added checks to prevent message stream from exceeding configured maximum size
|
||||
- Enhanced error messages to indicate validation failures
|
||||
|
||||
2. **Message Stream Protection**
|
||||
- `HandleClientCommunicationAsync()` now validates message stream size before writing received data
|
||||
- Prevents accumulation of large amounts of buffered data that could exhaust memory
|
||||
- Disconnects clients that exceed limits with proper error logging
|
||||
|
||||
3. **UDP Rate Limiting**
|
||||
- `HandleUdpDataAsync()` implements packet size validation
|
||||
- UDP packet rate limiting prevents flood attacks
|
||||
- Per-source tracking of packet rates
|
||||
|
||||
4. **Background Cleanup Task**
|
||||
- Added `CleanupRateLimitCacheAsync()` to periodically clean up rate limit entries
|
||||
- Prevents memory leaks from unbounded cache growth
|
||||
- Configurable cleanup interval
|
||||
|
||||
5. **Startup Integration**
|
||||
- Rate limit cache cleanup task integrated into server startup
|
||||
- Runs as background task with configurable interval
|
||||
|
||||
### 4. NetworkClient Hardening
|
||||
|
||||
1. **Length-Prefixed Message Validation**
|
||||
- Updated `ReceiveLengthPrefixedMessagesAsync()` to validate length prefixes using SecurityValidator
|
||||
- Added dedicated exception handling for malformed length messages
|
||||
- Improved error messages for validation failures
|
||||
|
||||
2. **Delimiter-Based Message Protection**
|
||||
- Enhanced `ReceiveDelimiterAsync()` with max message stream size enforcement
|
||||
- Added checks for accumulated message buffer size
|
||||
- Prevents memory exhaustion from large buffered data
|
||||
|
||||
### 5. HealthApiServer REST API Hardening
|
||||
|
||||
1. **Request Size Limits**
|
||||
- New `MaxRequestSize` property to enforce maximum HTTP request/response size
|
||||
- Defaults to 10 MB, configurable before server starts
|
||||
- Request line limited to 8192 bytes
|
||||
- Total headers limited to 65536 bytes
|
||||
|
||||
2. **HTTP Header Security**
|
||||
- Enhanced `ReadRequestLineAsync()` to validate for invalid control characters
|
||||
- Rejects null bytes (0x00) in request lines
|
||||
- `DrainHeadersAsync()` validates header size and detects null bytes
|
||||
- Prevents header-based injection attacks
|
||||
|
||||
3. **Response Security Hardening**
|
||||
- `WriteResponseAsync()` validates response body size against limit
|
||||
- Content-Type validation prevents injection of control characters
|
||||
- Additional security headers added:
|
||||
- `X-Frame-Options: DENY` - Clickjacking protection
|
||||
- `X-XSS-Protection: 1; mode=block` - XSS protection
|
||||
- `Strict-Transport-Security` - Forces HTTPS
|
||||
- `Pragma: no-cache` - Additional cache control
|
||||
- Improved error handling for oversized responses
|
||||
|
||||
## Attack Vectors Mitigated
|
||||
|
||||
### 1. Buffer Overflow Attacks
|
||||
- **Mitigation:** Length prefix validation, buffer size checks, message size limits
|
||||
- **Implementation:** SecurityValidator validates all length values before buffer allocation
|
||||
|
||||
### 2. Memory Exhaustion / Decompression Bombs
|
||||
- **Mitigation:** MaxMessageStreamSize configuration, per-message size limits
|
||||
- **Implementation:** Accumulated stream size checked before each write operation
|
||||
|
||||
### 3. Slow Loris / Slowhttptest Attacks
|
||||
- **Mitigation:** Request timeouts, header size limits
|
||||
- **Implementation:** Network timeouts and maximum header sizes enforced
|
||||
|
||||
### 4. UDP Flood / Amplification Attacks
|
||||
- **Mitigation:** UDP rate limiting, packet size validation
|
||||
- **Implementation:** Per-source rate limiting with configurable thresholds
|
||||
|
||||
### 5. HTTP Header Injection
|
||||
- **Mitigation:** HTTP header validation, null byte detection
|
||||
- **Implementation:** SecurityValidator checks for newlines and null bytes in headers
|
||||
|
||||
### 6. Malformed Packet Attacks
|
||||
- **Mitigation:** Strict validation of message framing and length prefixes
|
||||
- **Implementation:** Enhanced error handling with specific exceptions for malformed data
|
||||
|
||||
### 7. Information Leakage
|
||||
- **Mitigation:** Error message sanitization
|
||||
- **Implementation:** Sensitive error details hidden from clients, detailed logs for server admins
|
||||
|
||||
### 8. Rate Limiting / DoS
|
||||
- **Mitigation:** Per-client/source rate limiting capability
|
||||
- **Implementation:** SecurityValidator.CheckRateLimit with configurable thresholds
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
### Production Deployment
|
||||
```csharp
|
||||
var config = new Configuration
|
||||
{
|
||||
// Message constraints
|
||||
MAX_MESSAGE_SIZE = 100 * 1024 * 1024, // 100 MB
|
||||
MaxMessageStreamSize = 500 * 1024 * 1024, // 500 MB
|
||||
|
||||
// Rate limiting
|
||||
MaxMessagesPerSecond = 1000, // Per client
|
||||
MaxUdpPacketsPerSecond = 5000, // Per source
|
||||
MaxHealthApiRequestSize = 10 * 1024 * 1024, // 10 MB
|
||||
|
||||
// Security features
|
||||
EnableLengthPrefixValidation = true,
|
||||
EnableErrorMessageSanitization = true,
|
||||
EnableRateLimitCacheCleanup = true,
|
||||
EnableHttpHeaderValidation = true
|
||||
};
|
||||
```
|
||||
|
||||
### High-Security Deployment
|
||||
```csharp
|
||||
var config = new Configuration
|
||||
{
|
||||
MAX_MESSAGE_SIZE = 10 * 1024 * 1024, // 10 MB
|
||||
MaxMessageStreamSize = 50 * 1024 * 1024, // 50 MB
|
||||
MaxMessagesPerSecond = 100, // Strict rate limit
|
||||
MaxUdpPacketsPerSecond = 500, // Strict UDP limit
|
||||
MaxHealthApiRequestSize = 1 * 1024 * 1024, // 1 MB
|
||||
// All security features enabled by default
|
||||
};
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
Reference in New Issue
Block a user