From 17d72b9b3f08070f7fadae3b33a427b9a44ed3cb Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 1 Apr 2026 12:04:17 +0200 Subject: [PATCH] fix bootstrap rate limit, fix download timeout --- .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../TableStorage/TableConfigRepository.cs | 8 +++- .../DiagnosticsDownloadFunction.cs | 47 +++++++++++++++++-- .../Security/SecurityValidator.cs | 21 ++++++++- .../Models/Config/AdminConfiguration.cs | 15 ++++++ 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index a2891292..e2979846 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -752 +754 diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs index bc920e5d..40c79a58 100644 --- a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableConfigRepository.cs @@ -513,7 +513,9 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage { "DiagnosticsGlobalLogPathsJson", config.DiagnosticsGlobalLogPathsJson }, { "NvdApiKey", config.NvdApiKey }, { "VulnerabilityCorrelationEnabled", config.VulnerabilityCorrelationEnabled }, - { "VulnerabilityDataLastSyncUtc", config.VulnerabilityDataLastSyncUtc } + { "VulnerabilityDataLastSyncUtc", config.VulnerabilityDataLastSyncUtc }, + { "MaxDiagnosticsDownloadSizeMB", config.MaxDiagnosticsDownloadSizeMB }, + { "DiagnosticsDownloadTimeoutSeconds", config.DiagnosticsDownloadTimeoutSeconds } }; return entity; @@ -535,7 +537,9 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage DiagnosticsGlobalLogPathsJson = entity.GetString("DiagnosticsGlobalLogPathsJson"), NvdApiKey = entity.GetString("NvdApiKey"), VulnerabilityCorrelationEnabled = entity.GetBoolean("VulnerabilityCorrelationEnabled") ?? true, - VulnerabilityDataLastSyncUtc = entity.GetString("VulnerabilityDataLastSyncUtc") + VulnerabilityDataLastSyncUtc = entity.GetString("VulnerabilityDataLastSyncUtc"), + MaxDiagnosticsDownloadSizeMB = entity.GetInt32("MaxDiagnosticsDownloadSizeMB") ?? 500, + DiagnosticsDownloadTimeoutSeconds = entity.GetInt32("DiagnosticsDownloadTimeoutSeconds") ?? 120 }; } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs index a4ab2b79..a8bdcfe9 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs @@ -23,15 +23,18 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics { private readonly ILogger _logger; private readonly TenantConfigurationService _configService; + private readonly AdminConfigurationService _adminConfigService; private readonly TelemetryClient _telemetryClient; public DiagnosticsDownloadFunction( ILogger logger, TenantConfigurationService configService, + AdminConfigurationService adminConfigService, TelemetryClient telemetryClient) { _logger = logger; _configService = configService; + _adminConfigService = adminConfigService; _telemetryClient = telemetryClient; } @@ -98,14 +101,41 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics blobUrl = $"{containerSasUrl.TrimEnd('/')}/{blobName}"; } + // Load admin configuration for download limits + var adminConfig = await _adminConfigService.GetConfigurationAsync(); + var maxSizeBytes = (long)adminConfig.MaxDiagnosticsDownloadSizeMB * 1024 * 1024; + var timeoutSeconds = adminConfig.DiagnosticsDownloadTimeoutSeconds; + + using var cts = timeoutSeconds > 0 + ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) + : new CancellationTokenSource(); + // Download blob server-side and stream to client var sw = Stopwatch.StartNew(); var blobClient = new BlobClient(new Uri(blobUrl)); - var download = await blobClient.DownloadStreamingAsync(); + var download = await blobClient.DownloadStreamingAsync(cancellationToken: cts.Token); sw.Stop(); var contentLength = download.Value.Details.ContentLength; + // Enforce size limit before streaming (fast reject) + if (maxSizeBytes > 0 && contentLength > maxSizeBytes) + { + _logger.LogWarning( + "DiagnosticsDownload: Blob {BlobName} for tenant {TenantId} rejected — size {SizeBytes} bytes exceeds limit {MaxSizeBytes} bytes", + blobName, tenantId, contentLength, maxSizeBytes); + + download.Value.Content.Dispose(); + + var tooLarge = req.CreateResponse(HttpStatusCode.RequestEntityTooLarge); + await tooLarge.WriteAsJsonAsync(new + { + success = false, + message = $"Diagnostics package size ({contentLength / (1024 * 1024)} MB) exceeds the maximum allowed size ({adminConfig.MaxDiagnosticsDownloadSizeMB} MB)." + }); + return tooLarge; + } + _logger.LogInformation( "DiagnosticsDownload: Proxying blob {BlobName} for tenant {TenantId}, size {SizeBytes} bytes, fetch took {DurationMs}ms", blobName, tenantId, contentLength, sw.ElapsedMilliseconds); @@ -125,7 +155,7 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics ["DurationMs"] = sw.ElapsedMilliseconds }); - // Stream blob content to client + // Stream blob content to client (with timeout) var response = req.CreateResponse(HttpStatusCode.OK); response.Headers.Add("Content-Type", "application/octet-stream"); response.Headers.Add("Content-Disposition", $"attachment; filename=\"{blobName}\""); @@ -134,7 +164,7 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics response.Headers.Add("Content-Length", contentLength.ToString()); } - await download.Value.Content.CopyToAsync(response.Body); + await download.Value.Content.CopyToAsync(response.Body, cts.Token); return response; } catch (Azure.RequestFailedException ex) when (ex.Status == 404) @@ -144,6 +174,17 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics await notFoundResponse.WriteAsJsonAsync(new { success = false, message = "Diagnostics package not found." }); return notFoundResponse; } + catch (OperationCanceledException) + { + _logger.LogWarning("DiagnosticsDownload: Operation timed out or was cancelled for requested blob download"); + var timeoutResponse = req.CreateResponse(HttpStatusCode.GatewayTimeout); + await timeoutResponse.WriteAsJsonAsync(new + { + success = false, + message = "Diagnostics download timed out. The file may be too large or the connection is too slow." + }); + return timeoutResponse; + } catch (Exception ex) { _logger.LogError(ex, "Error proxying diagnostics download"); diff --git a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs index 773fb82d..9d72a868 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs @@ -173,6 +173,24 @@ namespace AutopilotMonitor.Functions.Security _logger.LogInformation("Bootstrap token auth accepted for tenant {TenantId} (code {ShortCode})", tenantId, bootstrapSession.ShortCode); + // Rate limit check for bootstrap auth (DoS protection) + var bsRateLimitValue = config.CustomRateLimitRequestsPerMinute ?? config.RateLimitRequestsPerMinute; + var bsRateLimitResult = _rateLimitService.CheckRateLimit( + $"bootstrap:{bootstrapTokenHeader}", + bsRateLimitValue + ); + + if (!bsRateLimitResult.IsAllowed) + { + return new SecurityValidationResult + { + IsValid = false, + StatusCode = HttpStatusCode.TooManyRequests, + ErrorMessage = "Rate limit exceeded", + RateLimitResult = bsRateLimitResult + }; + } + return new SecurityValidationResult { IsValid = true, @@ -180,7 +198,8 @@ namespace AutopilotMonitor.Functions.Security BootstrapShortCode = bootstrapSession.ShortCode, Manufacturer = bsManufacturer, Model = bsModel, - SerialNumber = bsSerial + SerialNumber = bsSerial, + RateLimitResult = bsRateLimitResult }; } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs index 70232dca..9ae743c5 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs @@ -141,6 +141,21 @@ namespace AutopilotMonitor.Shared.Models } } + // ===== DIAGNOSTICS DOWNLOAD LIMITS ===== + + /// + /// Maximum allowed diagnostics download size in MB. + /// Blobs exceeding this are rejected before streaming (413). + /// 0 = no limit. Default: 500 MB. + /// + public int MaxDiagnosticsDownloadSizeMB { get; set; } = 500; + + /// + /// Timeout in seconds for the entire diagnostics download+stream operation. + /// 0 = no timeout. Default: 120 seconds. + /// + public int DiagnosticsDownloadTimeoutSeconds { get; set; } = 120; + // ===== MCP ACCESS CONTROL ===== ///