From 7fae27f0daa3d54a6901f18e449a874d643be226 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Sat, 7 Feb 2026 21:26:49 +0100 Subject: [PATCH] handle OData filer injection --- .../Security/SecurityValidator.cs | 26 ++++++++++ .../Services/TableStorageService.cs | 47 +++++++++++++++++++ .../Services/TenantAdminsService.cs | 3 ++ 3 files changed, 76 insertions(+) diff --git a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs index 532637f0..f5d91ed3 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net; +using System.Text.RegularExpressions; using System.Threading.Tasks; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Shared.Models; @@ -15,6 +16,31 @@ namespace AutopilotMonitor.Functions.Security /// public class SecurityValidator { + private static readonly Regex GuidRegex = new Regex( + @"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + RegexOptions.Compiled); + + /// + /// Validates that a string is a valid GUID format. + /// Use this to prevent OData filter injection in Table Storage queries. + /// + public static bool IsValidGuid(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return Guid.TryParse(value, out _) && GuidRegex.IsMatch(value); + } + + /// + /// Validates that a value is a valid GUID and throws if not. + /// + public static void EnsureValidGuid(string? value, string parameterName) + { + if (!IsValidGuid(value)) + throw new ArgumentException($"Invalid {parameterName} format. Expected a valid GUID.", parameterName); + } + private readonly TenantConfigurationService _configService; private readonly RateLimitService _rateLimitService; private readonly ILogger _logger; diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs index f637e392..943c357d 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs @@ -1,5 +1,6 @@ using Azure; using Azure.Data.Tables; +using AutopilotMonitor.Functions.Security; using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Extensions.Configuration; @@ -82,6 +83,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task StoreSessionAsync(SessionRegistration registration) { + SecurityValidator.EnsureValidGuid(registration.TenantId, "TenantId"); + SecurityValidator.EnsureValidGuid(registration.SessionId, "SessionId"); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions); @@ -122,6 +126,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task StoreEventAsync(EnrollmentEvent evt) { + SecurityValidator.EnsureValidGuid(evt.TenantId, "TenantId"); + SecurityValidator.EnsureValidGuid(evt.SessionId, "SessionId"); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Events); @@ -166,6 +173,8 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetSessionsAsync(string tenantId, int maxResults = 100) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions); @@ -248,6 +257,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task UpdateSessionStatusAsync(string tenantId, string sessionId, SessionStatus status, EnrollmentPhase? currentPhase = null, string? failureReason = null) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); + const int maxRetries = 3; int retryCount = 0; @@ -327,6 +339,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetSessionEventsAsync(string tenantId, string sessionId, int maxResults = 1000) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Events); @@ -439,6 +454,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task DeleteSessionEventsAsync(string tenantId, string sessionId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Events); @@ -501,6 +519,8 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetAuditLogsAsync(string tenantId, int maxResults = 100) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.AuditLogs); @@ -581,6 +601,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetUsageMetricsSnapshotAsync(string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100) { + if (!string.IsNullOrEmpty(tenantId)) + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.UsageMetrics); @@ -644,6 +667,8 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.Sessions); @@ -740,6 +765,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetRuleResultsAsync(string tenantId, string sessionId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.RuleResults); @@ -828,6 +856,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetGatherRulesAsync(string partitionKey) { + if (partitionKey != "global") + SecurityValidator.EnsureValidGuid(partitionKey, nameof(partitionKey)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GatherRules); @@ -945,6 +976,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetAnalyzeRulesAsync(string partitionKey) { + if (partitionKey != "global") + SecurityValidator.EnsureValidGuid(partitionKey, nameof(partitionKey)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.AnalyzeRules); @@ -1053,6 +1087,8 @@ namespace AutopilotMonitor.Functions.Services /// public async Task> GetAppInstallSummariesByTenantAsync(string tenantId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.AppInstallSummaries); @@ -1078,6 +1114,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.AppInstallSummaries); @@ -1105,6 +1144,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task DeleteSessionRuleResultsAsync(string tenantId, string sessionId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + SecurityValidator.EnsureValidGuid(sessionId, nameof(sessionId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.RuleResults); @@ -1297,6 +1339,8 @@ namespace AutopilotMonitor.Functions.Services /// public async Task GetUserActivityMetricsAsync(string tenantId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.UserActivity); @@ -1405,6 +1449,9 @@ namespace AutopilotMonitor.Functions.Services /// public async Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date) { + if (!string.IsNullOrEmpty(tenantId) && tenantId != "global") + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + try { var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.UserActivity); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TenantAdminsService.cs b/src/Backend/AutopilotMonitor.Functions/Services/TenantAdminsService.cs index 14fb8eb5..9af2f0e9 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TenantAdminsService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TenantAdminsService.cs @@ -1,3 +1,4 @@ +using AutopilotMonitor.Functions.Security; using AutopilotMonitor.Shared; using Azure.Data.Tables; using Microsoft.Extensions.Caching.Memory; @@ -207,6 +208,8 @@ public class TenantAdminsService /// public async Task> GetTenantAdminsAsync(string tenantId) { + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + tenantId = tenantId.ToLowerInvariant(); var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.TenantAdmins);