fix(app): harden account and workflow state

This commit is contained in:
cesnimda
2026-08-24 20:21:09 +02:00
parent e7cacad7d6
commit dca5daa1a2
32 changed files with 811 additions and 86 deletions
@@ -11,6 +11,7 @@ namespace JobTrackerApi.Services.JobImport;
public sealed class JobImportService
{
private const int MaxDownloadBytes = 4_000_000;
private readonly IHttpClientFactory _httpClientFactory;
private readonly UniversalJobParser _universal;
private readonly IEnumerable<IJobSitePlugin> _plugins;
@@ -124,10 +125,25 @@ public sealed class JobImportService
// Still read: many sites omit content-type. Best-effort.
}
// Cap to avoid huge downloads.
var bytes = await res.Content.ReadAsByteArrayAsync(cancellationToken);
if (bytes.Length > 4_000_000) return null;
return System.Text.Encoding.UTF8.GetString(bytes);
// Enforce the cap while streaming. Checking after ReadAsByteArrayAsync allowed an
// arbitrarily large (and automatically decompressed) response to consume memory first.
if (res.Content.Headers.ContentLength is > MaxDownloadBytes) return null;
await using var body = await res.Content.ReadAsStreamAsync(cancellationToken);
using var bounded = new MemoryStream(capacity: res.Content.Headers.ContentLength is > 0
? (int)Math.Min(res.Content.Headers.ContentLength.Value, MaxDownloadBytes)
: 0);
var buffer = new byte[64 * 1024];
var total = 0;
while (true)
{
var remainingWithSentinel = MaxDownloadBytes - total + 1;
var read = await body.ReadAsync(buffer.AsMemory(0, Math.Min(buffer.Length, remainingWithSentinel)), cancellationToken);
if (read == 0) break;
total += read;
if (total > MaxDownloadBytes) return null;
await bounded.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
}
return System.Text.Encoding.UTF8.GetString(bounded.GetBuffer(), 0, total);
}
private async Task<UrlValidationResult> ValidateUrlAsync(string? url, CancellationToken cancellationToken)