security: enforce admin authorization, fix error leakage, prevent injection

- H1: Require TenantAdmin or GalacticAdmin for mutative tenant endpoints
  (DeleteSession, MarkSessionFailed, UpdateTenantConfiguration,
  AnalyzeRules CRUD, GatherRules CRUD)
- M1: Add EnsureValidGuid validation to TenantOffboardFunction to
  prevent OData injection via tenantId route parameter
- M2: Remove ex.Message from error responses in UsageMetrics,
  PlatformUsageMetrics, TriggerMaintenance, TenantOffboard,
  ReseedFromGitHub to prevent internal error leakage
- M4: Use requireGalacticAdmin on ProtectedRoute and server-derived
  user.isGalacticAdmin instead of localStorage for admin-configuration
- N1: Add 1-second Regex timeout to agent GatherRuleExecutor and
  ImeLogTracker to prevent ReDoS from malicious patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-02-28 20:46:50 +01:00
parent 59d7988092
commit 4e21ae3008
13 changed files with 192 additions and 26 deletions
@@ -667,7 +667,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
Regex pattern;
try
{
pattern = new Regex(patternStr, RegexOptions.Compiled);
pattern = new Regex(patternStr, RegexOptions.Compiled, TimeSpan.FromSeconds(1));
}
catch (Exception ex)
{
@@ -711,7 +711,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
if (!CmTraceLogParser.TryParseLine(line, out entry))
continue;
var match = pattern.Match(entry.Message);
Match match;
try
{
match = pattern.Match(entry.Message);
}
catch (RegexMatchTimeoutException)
{
continue;
}
if (!match.Success)
continue;
@@ -140,7 +140,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
{
// Replace {GUID} placeholder with actual GUID capture regex
var regexStr = pattern.Pattern.Replace("{GUID}", GuidPattern);
var regex = new Regex(regexStr, RegexOptions.IgnoreCase | RegexOptions.Compiled);
var regex = new Regex(regexStr, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
var compiled = new CompiledPattern
{
@@ -439,11 +439,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
// Match against active patterns
foreach (var pattern in _activePatterns)
{
var match = pattern.Regex.Match(messageToMatch);
if (match.Success)
try
{
WriteMatchLog(filePath, line, pattern.PatternId);
HandlePatternMatch(pattern, match, messageToMatch);
var match = pattern.Regex.Match(messageToMatch);
if (match.Success)
{
WriteMatchLog(filePath, line, pattern.PatternId);
HandlePatternMatch(pattern, match, messageToMatch);
}
}
catch (RegexMatchTimeoutException)
{
// Skip this pattern match to prevent ReDoS
}
}
}
@@ -16,11 +16,19 @@ namespace AutopilotMonitor.Functions.Functions
{
private readonly ILogger<AnalyzeRulesFunction> _logger;
private readonly AnalyzeRuleService _ruleService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public AnalyzeRulesFunction(ILogger<AnalyzeRulesFunction> logger, AnalyzeRuleService ruleService)
public AnalyzeRulesFunction(
ILogger<AnalyzeRulesFunction> logger,
AnalyzeRuleService ruleService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_ruleService = ruleService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("GetAnalyzeRules")]
@@ -54,6 +62,17 @@ namespace AutopilotMonitor.Functions.Functions
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
if (req.Headers.TryGetValues("Content-Length", out var clValues)
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
&& contentLength > 1_048_576) // 1 MB limit
@@ -92,6 +111,17 @@ namespace AutopilotMonitor.Functions.Functions
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
if (req.Headers.TryGetValues("Content-Length", out var clValues2)
&& long.TryParse(clValues2.FirstOrDefault(), out var contentLength2)
&& contentLength2 > 1_048_576) // 1 MB limit
@@ -131,6 +161,17 @@ namespace AutopilotMonitor.Functions.Functions
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Load the rule to determine its type (built-in/community vs. custom)
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
var rule = rules.FirstOrDefault(r => r.RuleId == ruleId);
@@ -11,11 +11,19 @@ namespace AutopilotMonitor.Functions.Functions
{
private readonly ILogger<DeleteSessionFunction> _logger;
private readonly TableStorageService _storageService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public DeleteSessionFunction(ILogger<DeleteSessionFunction> logger, TableStorageService storageService)
public DeleteSessionFunction(
ILogger<DeleteSessionFunction> logger,
TableStorageService storageService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_storageService = storageService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("DeleteSession")]
@@ -43,6 +51,21 @@ namespace AutopilotMonitor.Functions.Functions
var tenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
// Require Tenant Admin or Galactic Admin for destructive operations
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to delete session {sessionId}");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
{
success = false,
message = "Access denied. Tenant Admin or Galactic Admin role required."
});
return forbiddenResponse;
}
_logger.LogInformation($"Deleting session {sessionId} for tenant {tenantId} by user {userIdentifier}");
// Delete all related data for this session
@@ -16,11 +16,19 @@ namespace AutopilotMonitor.Functions.Functions
{
private readonly ILogger<GatherRulesFunction> _logger;
private readonly GatherRuleService _ruleService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public GatherRulesFunction(ILogger<GatherRulesFunction> logger, GatherRuleService ruleService)
public GatherRulesFunction(
ILogger<GatherRulesFunction> logger,
GatherRuleService ruleService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_ruleService = ruleService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("GetGatherRules")]
@@ -54,6 +62,17 @@ namespace AutopilotMonitor.Functions.Functions
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
if (req.Headers.TryGetValues("Content-Length", out var clValues)
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
&& contentLength > 1_048_576) // 1 MB limit
@@ -92,6 +111,17 @@ namespace AutopilotMonitor.Functions.Functions
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
if (req.Headers.TryGetValues("Content-Length", out var clValues2)
&& long.TryParse(clValues2.FirstOrDefault(), out var contentLength2)
&& contentLength2 > 1_048_576) // 1 MB limit
@@ -131,6 +161,17 @@ namespace AutopilotMonitor.Functions.Functions
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Load the rule to determine its type (built-in/community vs. custom)
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
var rule = rules.FirstOrDefault(r => r.RuleId == ruleId);
@@ -13,11 +13,19 @@ namespace AutopilotMonitor.Functions.Functions
{
private readonly ILogger<MarkSessionFailedFunction> _logger;
private readonly TableStorageService _storageService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public MarkSessionFailedFunction(ILogger<MarkSessionFailedFunction> logger, TableStorageService storageService)
public MarkSessionFailedFunction(
ILogger<MarkSessionFailedFunction> logger,
TableStorageService storageService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_storageService = storageService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("MarkSessionFailed")]
@@ -45,6 +53,21 @@ namespace AutopilotMonitor.Functions.Functions
string tenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// Require Tenant Admin or Galactic Admin for status modifications
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to mark session {sessionId} as failed");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
{
success = false,
message = "Access denied. Tenant Admin or Galactic Admin role required."
});
return new MarkSessionFailedOutput { HttpResponse = forbiddenResponse };
}
_logger.LogInformation($"Marking session {sessionId} as failed for tenant {tenantId} by user {userIdentifier}");
// Update session status to Failed with manual failure reason
@@ -85,8 +85,7 @@ namespace AutopilotMonitor.Functions.Functions
await errorResponse.WriteAsJsonAsync(new
{
success = false,
message = "Failed to compute usage metrics",
error = ex.Message
message = "Failed to compute usage metrics"
});
return errorResponse;
@@ -115,7 +115,7 @@ namespace AutopilotMonitor.Functions.Functions
await response.WriteAsJsonAsync(new
{
success = false,
message = $"Failed to fetch rules from GitHub: {ex.Message}. GitHub CDN may cache responses for up to 5 minutes after a merge."
message = "Failed to fetch rules from GitHub. GitHub CDN may cache responses for up to 5 minutes after a merge."
});
return response;
}
@@ -1,6 +1,7 @@
using System.Net;
using AutopilotMonitor.Functions.Extensions;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Security;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared;
using Azure.Data.Tables;
@@ -75,6 +76,9 @@ public class TenantOffboardFunction
return forbiddenResponse;
}
// Validate tenantId format to prevent OData injection
SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId));
_logger.LogWarning($"TENANT OFFBOARD initiated for tenant {tenantId} by {upn}");
var result = new OffboardResult { TenantId = tenantId, InitiatedBy = upn!, InitiatedAt = DateTime.UtcNow };
@@ -122,7 +126,7 @@ public class TenantOffboardFunction
{
_logger.LogError(ex, $"Tenant offboard failed for tenant {tenantId}");
result.Success = false;
result.Error = ex.Message;
result.Error = "Internal server error";
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(result);
@@ -121,7 +121,7 @@ namespace AutopilotMonitor.Functions.Functions
await errorResponse.WriteAsJsonAsync(new
{
success = false,
error = ex.Message
message = "Internal server error"
});
return errorResponse;
@@ -16,13 +16,19 @@ namespace AutopilotMonitor.Functions.Functions
{
private readonly ILogger<UpdateTenantConfigurationFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public UpdateTenantConfigurationFunction(
ILogger<UpdateTenantConfigurationFunction> logger,
TenantConfigurationService configService)
TenantConfigurationService configService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_configService = configService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("UpdateTenantConfiguration")]
@@ -62,6 +68,21 @@ namespace AutopilotMonitor.Functions.Functions
return forbiddenResponse;
}
// Require Tenant Admin or Galactic Admin for configuration changes
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to update configuration for tenant {tenantId}");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
{
success = false,
message = "Access denied. Tenant Admin or Galactic Admin role required."
});
return forbiddenResponse;
}
_logger.LogInformation($"UpdateTenantConfiguration: {tenantId} by user {userIdentifier}");
// Parse request body
@@ -69,8 +69,7 @@ namespace AutopilotMonitor.Functions.Functions
await errorResponse.WriteAsJsonAsync(new
{
success = false,
message = "Failed to compute tenant usage metrics",
error = ex.Message
message = "Failed to compute tenant usage metrics"
});
return errorResponse;
@@ -28,7 +28,7 @@ interface AdminConfiguration {
export default function AdminConfigurationPage() {
const router = useRouter();
const { getAccessToken } = useAuth();
const { getAccessToken, user } = useAuth();
const [galacticAdminMode, setGalacticAdminMode] = useState(false);
// Diagnostics Log Paths state
const [globalDiagPaths, setGlobalDiagPaths] = useState<DiagnosticsLogPath[]>([]);
@@ -53,16 +53,16 @@ export default function AdminConfigurationPage() {
// Preview Whitelist state
const [previewApproved, setPreviewApproved] = useState<Set<string>>(new Set());
// Load galactic admin mode from localStorage
// Load galactic admin mode - use server-derived isGalacticAdmin as authoritative source
useEffect(() => {
const galacticMode = localStorage.getItem("galacticAdminMode") === "true";
const galacticMode = user?.isGalacticAdmin === true;
setGalacticAdminMode(galacticMode);
// Redirect if not in galactic admin mode
if (!galacticMode) {
// Redirect if not a galactic admin
if (user && !galacticMode) {
router.push("/dashboard");
}
}, [router]);
}, [router, user]);
// Fetch admin configuration
useEffect(() => {
@@ -263,7 +263,7 @@ export default function AdminConfigurationPage() {
}
return (
<ProtectedRoute>
<ProtectedRoute requireGalacticAdmin>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */}
<header className="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700">