diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DiagnosticsPathGuards.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DiagnosticsPathGuards.cs index f3b6e3ad..23388e25 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DiagnosticsPathGuards.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DiagnosticsPathGuards.cs @@ -106,6 +106,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors normalizedDir = Path.GetFullPath(expanded); } + // Symlink / junction / reparse point detection — fail-closed + if (IsSymlinkOrReparsePoint(normalizedDir)) + return false; + // C:\Users block always applies (even in unrestricted mode) if (normalizedDir.StartsWith(BlockedUsersPrefix, StringComparison.OrdinalIgnoreCase) && (normalizedDir.Length == BlockedUsersPrefix.Length || @@ -152,5 +156,45 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors return false; } } + + /// + /// Checks whether any component of the path (directories or file) is a symlink, + /// junction, or other reparse point. Returns true if a reparse point is detected. + /// Fail-closed: returns true on any error. + /// + private static bool IsSymlinkOrReparsePoint(string path) + { + try + { + // Walk up the directory tree checking each component + var current = path; + while (!string.IsNullOrEmpty(current)) + { + var dirInfo = new DirectoryInfo(current); + if (dirInfo.Exists && dirInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + return true; + + var parent = Path.GetDirectoryName(current); + if (parent == current) // root reached + break; + current = parent; + } + + // Check the target path itself (if it's a file) + if (File.Exists(path)) + { + var fileInfo = new FileInfo(path); + if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + return true; + } + + return false; + } + catch + { + // Fail-closed: treat errors as suspicious + return true; + } + } } } diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 4eb9d0d9..ecb9d0c1 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -580 +581 diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs index 10cb1a1e..4903d4e5 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs @@ -135,6 +135,16 @@ namespace AutopilotMonitor.Functions.Functions.Feedback var principal = req.FunctionContext.GetUser(); string displayName = principal?.GetDisplayName() ?? upn; + // Request body size limit (1 MB) + if (req.Headers.TryGetValues("Content-Length", out var clValues) + && long.TryParse(clValues.FirstOrDefault(), out var contentLength) + && contentLength > 1_048_576) + { + var tooLarge = req.CreateResponse(HttpStatusCode.BadRequest); + await tooLarge.WriteAsJsonAsync(new { success = false, message = "Request body too large" }); + return tooLarge; + } + var body = await req.ReadFromJsonAsync(); if (body == null) { diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs index 74c1092c..04d93dd2 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs @@ -101,6 +101,15 @@ namespace AutopilotMonitor.Functions.Functions.Ingest /// internal async Task ProcessReportErrorAsync(HttpRequestData req, string tenantId) { + // Request body size limit (1 MB) + if (req.Headers.TryGetValues("Content-Length", out var clValues) + && long.TryParse(clValues.FirstOrDefault(), out var contentLength) + && contentLength > 1_048_576) + { + _logger.LogWarning("ReportAgentError: Request body too large ({ContentLength} bytes) from tenant {TenantId}", contentLength, tenantId); + return req.CreateResponse(HttpStatusCode.OK); // Still 200 — agent must not retry + } + // Parse the report body AgentErrorReport? report = null; try diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs index d5d984ed..fa0cb6ae 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs @@ -49,6 +49,16 @@ namespace AutopilotMonitor.Functions.Functions.Reports // Still needed: GA check for audit log skip logic var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier); + // Request body size limit (1 MB) + if (req.Headers.TryGetValues("Content-Length", out var clValues) + && long.TryParse(clValues.FirstOrDefault(), out var contentLength) + && contentLength > 1_048_576) + { + var tooLarge = req.CreateResponse(HttpStatusCode.BadRequest); + await tooLarge.WriteAsJsonAsync(new { success = false, message = "Request body too large" }); + return tooLarge; + } + // Parse request body var request = await req.ReadFromJsonAsync(); if (request == null)