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);