Harden image URL validation against followed redirects
This commit is contained in:
@@ -122,6 +122,8 @@ internal static class EndpointHelpers
|
||||
{
|
||||
using var head = new HttpRequestMessage(HttpMethod.Head, uri);
|
||||
var headResp = await client.SendAsync(head, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
if (WasRedirected(uri, headResp))
|
||||
return false;
|
||||
if (headResp is { IsSuccessStatusCode: true, StatusCode: not System.Net.HttpStatusCode.Redirect })
|
||||
{
|
||||
if (headResp.Content.Headers.ContentLength is > MaxImageBytes)
|
||||
@@ -142,6 +144,8 @@ internal static class EndpointHelpers
|
||||
using var get = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
get.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(0, 1023);
|
||||
var resp = await client.SendAsync(get, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
if (WasRedirected(uri, resp))
|
||||
return false;
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
return false;
|
||||
if (resp.StatusCode is System.Net.HttpStatusCode.Redirect)
|
||||
@@ -179,6 +183,15 @@ internal static class EndpointHelpers
|
||||
|
||||
private const long MaxImageBytes = 5 * 1024 * 1024; // 5 MB guard
|
||||
|
||||
private static bool WasRedirected(Uri requestedUri, HttpResponseMessage response)
|
||||
{
|
||||
var finalUri = response.RequestMessage?.RequestUri;
|
||||
if (finalUri is null)
|
||||
return false;
|
||||
|
||||
return !requestedUri.Equals(finalUri);
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSafePublicHostAsync(Uri uri, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -113,6 +113,14 @@ public class HelperTests
|
||||
Assert.False(tooLarge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsReachableImageAsync_rejects_followed_redirect_chain()
|
||||
{
|
||||
var handler = new RedirectFollowingHandler();
|
||||
var reachable = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(new StubHttpMessageHandler()), handler);
|
||||
Assert.False(reachable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsReachableImageAsync_rejects_non_image_content()
|
||||
{
|
||||
@@ -195,4 +203,29 @@ public class HelperTests
|
||||
public string ContentRootPath { get; set; } = "";
|
||||
public IFileProvider ContentRootFileProvider { get; set; } = null!;
|
||||
}
|
||||
|
||||
private sealed class RedirectFollowingHandler : HttpMessageHandler
|
||||
{
|
||||
private static readonly Uri Source = new("http://example.com/img.png");
|
||||
private static readonly Uri Destination = new("http://example.com/final.png");
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.RequestUri != Source)
|
||||
{
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request });
|
||||
}
|
||||
|
||||
var redirectedRequest = new HttpRequestMessage(request.Method, Destination);
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
RequestMessage = redirectedRequest,
|
||||
Content = new ByteArrayContent("PNG"u8.ToArray())
|
||||
};
|
||||
response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
|
||||
response.Content.Headers.ContentLength = 3;
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connect
|
||||
|
||||
builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); });
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddHttpClient("imageValidation").ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false });
|
||||
builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(dataProtectionDirectory));
|
||||
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
|
||||
|
||||
Reference in New Issue
Block a user