Added more security
This commit is contained in:
@@ -19,6 +19,17 @@ namespace EonaCat.Connections.Helpers
|
|||||||
private volatile bool _running;
|
private volatile bool _running;
|
||||||
private readonly Func<string> _getHealthJson;
|
private readonly Func<string> _getHealthJson;
|
||||||
private readonly Func<string> _getStatusJson;
|
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>
|
/// <summary>
|
||||||
/// Gets the actual port the server is listening on.
|
/// 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 sb = new StringBuilder();
|
||||||
var buffer = new byte[1];
|
var buffer = new byte[1];
|
||||||
var prev = (byte)0;
|
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);
|
int read = await stream.ReadAsync(buffer, 0, 1).ConfigureAwait(false);
|
||||||
if (read == 0)
|
if (read == 0)
|
||||||
@@ -222,6 +234,12 @@ namespace EonaCat.Connections.Helpers
|
|||||||
break;
|
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')
|
if (b != (byte)'\r')
|
||||||
{
|
{
|
||||||
sb.Append((char)b);
|
sb.Append((char)b);
|
||||||
@@ -230,16 +248,24 @@ namespace EonaCat.Connections.Helpers
|
|||||||
prev = b;
|
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();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task DrainHeadersAsync(NetworkStream stream)
|
private async Task DrainHeadersAsync(NetworkStream stream)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
var buffer = new byte[1];
|
var buffer = new byte[1];
|
||||||
int consecutiveNewlines = 0;
|
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);
|
int read = await stream.ReadAsync(buffer, 0, 1).ConfigureAwait(false);
|
||||||
if (read == 0)
|
if (read == 0)
|
||||||
@@ -247,6 +273,14 @@ namespace EonaCat.Connections.Helpers
|
|||||||
break;
|
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')
|
if (buffer[0] == (byte)'\n')
|
||||||
{
|
{
|
||||||
consecutiveNewlines++;
|
consecutiveNewlines++;
|
||||||
@@ -256,18 +290,43 @@ namespace EonaCat.Connections.Helpers
|
|||||||
consecutiveNewlines = 0;
|
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);
|
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" +
|
var header = $"HTTP/1.1 {statusCode} {statusText}\r\n" +
|
||||||
$"Content-Type: {contentType}; charset=utf-8\r\n" +
|
$"Content-Type: {contentType}; charset=utf-8\r\n" +
|
||||||
$"Content-Length: {bodyBytes.Length}\r\n" +
|
$"Content-Length: {bodyBytes.Length}\r\n" +
|
||||||
"Access-Control-Allow-Origin: *\r\n" +
|
"Access-Control-Allow-Origin: *\r\n" +
|
||||||
"Connection: close\r\n" +
|
"Connection: close\r\n" +
|
||||||
"X-Content-Type-Options: nosniff\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";
|
"\r\n";
|
||||||
|
|
||||||
var headerBytes = Encoding.ASCII.GetBytes(header);
|
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>
|
/// </summary>
|
||||||
public double ReadTimeoutSeconds { get; set; } = 300;
|
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()
|
internal RemoteCertificateValidationCallback GetRemoteCertificateValidationCallback()
|
||||||
{
|
{
|
||||||
return CertificateValidation;
|
return CertificateValidation;
|
||||||
|
|||||||
@@ -1209,9 +1209,10 @@ namespace EonaCat.Connections
|
|||||||
|
|
||||||
long length = ParseLengthPrefix(lengthBuffer, prefixSize, _config.UseBigEndian);
|
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);
|
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");
|
RecordError(ex, "Read timeout during length-prefixed receive - stream may have hung");
|
||||||
await DisconnectClientAsync(DisconnectReason.Timeout, ex);
|
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)
|
catch (SocketException socketEx)
|
||||||
{
|
{
|
||||||
DebugConnection($"Socket error in length-prefixed receive: {socketEx.SocketErrorCode} - {socketEx.Message}");
|
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" }}");
|
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
|
var reason = socketEx.SocketErrorCode == SocketError.ConnectionReset || socketEx.SocketErrorCode == SocketError.ConnectionAborted
|
||||||
? DisconnectReason.RemoteClosed
|
? DisconnectReason.RemoteClosed
|
||||||
: DisconnectReason.Error;
|
: DisconnectReason.Error;
|
||||||
@@ -1418,9 +1427,16 @@ namespace EonaCat.Connections
|
|||||||
|
|
||||||
memory.Write(buffer, 0, read);
|
memory.Write(buffer, 0, read);
|
||||||
|
|
||||||
|
// Security: Check both individual message size and accumulated stream size
|
||||||
if (memory.Length > _config.MAX_MESSAGE_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))
|
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");
|
RecordError(ex, "Read timeout in delimiter receive - stream may be unresponsive");
|
||||||
await DisconnectClientAsync(DisconnectReason.Timeout, ex);
|
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)
|
catch (SocketException socketEx)
|
||||||
{
|
{
|
||||||
DebugConnection($"Socket error in delimiter receive: {socketEx.SocketErrorCode} - {socketEx.Message}");
|
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);
|
_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)
|
if (_config.EnableAutoHtmlReports)
|
||||||
{
|
{
|
||||||
StatusPage.StartAutoHtmlReport(
|
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)
|
private async Task CleanupUdpClientsAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
@@ -1131,6 +1162,22 @@ namespace EonaCat.Connections
|
|||||||
{
|
{
|
||||||
var clientKey = result.RemoteEndPoint.ToString();
|
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))
|
if (!_clients.TryGetValue(clientKey, out var client))
|
||||||
{
|
{
|
||||||
client = new Connection
|
client = new Connection
|
||||||
@@ -1222,6 +1269,17 @@ namespace EonaCat.Connections
|
|||||||
return;
|
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);
|
UpdateLastActive(client);
|
||||||
await messageStream.WriteAsync(readBuffer, 0, bytesRead, token);
|
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);
|
RecordError(ioEx, $"IO error in communication loop: {ioEx.Message}", client.Id, client.Nickname);
|
||||||
await DisconnectClientAsync(client.Id, DisconnectReason.Error, ioEx);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
DebugConnection($"Error in communication loop for client {client.Nickname} ({client.Id}): {ex.Message}, Disconnecting");
|
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);
|
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;
|
int messageLength = (int)messageLengthValue;
|
||||||
@@ -1343,6 +1409,13 @@ namespace EonaCat.Connections
|
|||||||
return false;
|
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];
|
message = new byte[messageLength];
|
||||||
Buffer.BlockCopy(buffer, prefixSize, message, 0, 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
|
## Performance Considerations
|
||||||
|
|
||||||
### Optimization Tips
|
### Optimization Tips
|
||||||
|
|||||||
Reference in New Issue
Block a user