Security: request size limit, symlink detection

This commit is contained in:
Oliver Kieselbach
2026-03-26 08:07:02 +01:00
parent ffc2f1f67f
commit e8e586aa19
5 changed files with 74 additions and 1 deletions
@@ -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;
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
}
}
}
@@ -1 +1 @@
580
581
@@ -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<FeedbackRequest>();
if (body == null)
{
@@ -101,6 +101,15 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
/// </summary>
internal async Task<HttpResponseData> 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
@@ -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<SubmitSessionReportRequest>();
if (request == null)