[backend/core] Fix link verification for sites served with Transfer-Encoding: chunked

This commit is contained in:
Laura Hausmann 2025-03-06 15:52:14 +01:00
parent 107160c690
commit a4717da8ab
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 41 additions and 36 deletions

View file

@ -28,4 +28,35 @@ public static class StreamExtensions
ValueTask<int> DoReadAsync() => source.ReadAsync(new Memory<byte>(buffer), cancellationToken); ValueTask<int> DoReadAsync() => source.ReadAsync(new Memory<byte>(buffer), cancellationToken);
} }
/// <summary>
/// We can't trust the Content-Length header, and it might be null.
/// This makes sure that we only ever read up to maxLength into memory.
/// </summary>
/// <param name="stream">The response content stream</param>
/// <param name="maxLength">The maximum length to buffer (null = unlimited)</param>
/// <param name="contentLength">The content length, if known</param>
/// <param name="token">A CancellationToken, if applicable</param>
/// <returns>Either a buffered MemoryStream, or Stream.Null</returns>
public static async Task<Stream> GetSafeStreamOrNullAsync(
this Stream stream, long? maxLength, long? contentLength, CancellationToken token = default
)
{
if (maxLength is 0) return Stream.Null;
if (contentLength > maxLength) return Stream.Null;
MemoryStream buf = new();
if (contentLength < maxLength)
maxLength = contentLength.Value;
await stream.CopyToAsync(buf, maxLength, token);
if (maxLength == null || buf.Length <= maxLength)
{
buf.Seek(0, SeekOrigin.Begin);
return buf;
}
await buf.DisposeAsync();
return Stream.Null;
}
} }

View file

@ -120,7 +120,7 @@ public class DriveService(
? storageConfig.Value.MaxCacheSizeBytes ? storageConfig.Value.MaxCacheSizeBytes
: 0; : 0;
var stream = await GetSafeStreamOrNullAsync(input, maxLength, res.Content.Headers.ContentLength); var stream = await input.GetSafeStreamOrNullAsync(maxLength, res.Content.Headers.ContentLength);
try try
{ {
return await StoreFileAsync(stream, user, request, skipImageProcessing); return await StoreFileAsync(stream, user, request, skipImageProcessing);
@ -629,37 +629,6 @@ public class DriveService(
int GetTargetRes() => config.TargetRes ?? throw new Exception("TargetRes is required to encode images"); int GetTargetRes() => config.TargetRes ?? throw new Exception("TargetRes is required to encode images");
// @formatter:on // @formatter:on
} }
/// <summary>
/// We can't trust the Content-Length header, and it might be null.
/// This makes sure that we only ever read up to maxLength into memory.
/// </summary>
/// <param name="stream">The response content stream</param>
/// <param name="maxLength">The maximum length to buffer (null = unlimited)</param>
/// <param name="contentLength">The content length, if known</param>
/// <param name="token">A CancellationToken, if applicable</param>
/// <returns>Either a buffered MemoryStream, or Stream.Null</returns>
private static async Task<Stream> GetSafeStreamOrNullAsync(
Stream stream, long? maxLength, long? contentLength, CancellationToken token = default
)
{
if (maxLength is 0) return Stream.Null;
if (contentLength > maxLength) return Stream.Null;
MemoryStream buf = new();
if (contentLength < maxLength)
maxLength = contentLength.Value;
await stream.CopyToAsync(buf, maxLength, token);
if (maxLength == null || buf.Length <= maxLength)
{
buf.Seek(0, SeekOrigin.Begin);
return buf;
}
await buf.DisposeAsync();
return Stream.Null;
}
} }
public class DriveFileCreationRequest public class DriveFileCreationRequest

View file

@ -1203,13 +1203,14 @@ public class UserService(
try try
{ {
var res = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); const int maxLength = 1_000_000;
var res = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
if ( if (
res is not res is not
{ {
IsSuccessStatusCode: true, IsSuccessStatusCode: true,
Content.Headers: { ContentType.MediaType: "text/html", ContentLength: <= 1_000_000 } Content.Headers: { ContentType.MediaType: "text/html", ContentLength: null or <= maxLength }
} }
) )
{ {
@ -1220,9 +1221,13 @@ public class UserService(
continue; continue;
} }
var html = await res.Content.ReadAsStringAsync(); var contentLength = res.Content.Headers.ContentLength;
var document = await new HtmlParser().ParseDocumentAsync(html); var stream = await res.Content.ReadAsStreamAsync()
.ContinueWithResult(p => p.GetSafeStreamOrNullAsync(maxLength, contentLength));
if (stream == Stream.Null) throw new Exception("Response size limit exceeded");
var document = await new HtmlParser().ParseDocumentAsync(stream);
var headLinks = document.Head?.Children.Where(el => el.NodeName.ToLower() == "link").ToList() ?? []; var headLinks = document.Head?.Children.Where(el => el.NodeName.ToLower() == "link").ToList() ?? [];
userProfileField.IsVerified = userProfileField.IsVerified =