Skip to content

Commit 2394539

Browse files
Only block image requests and cache generated tokens.
1 parent 50db0b2 commit 2394539

3 files changed

Lines changed: 61 additions & 17 deletions

File tree

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ private static readonly ConcurrentTLruCache<string, ImageMetadata> SourceMetadat
4141
private static readonly ConcurrentTLruCache<string, (IImageCacheResolver, ImageCacheMetadata)> CacheResolverLru
4242
= new(1024, TimeSpan.FromSeconds(30));
4343

44+
/// <summary>
45+
/// Used to temporarily store cached HMAC-s to reduce the overhead of HMAC token generation.
46+
/// </summary>
47+
private static readonly ConcurrentTLruCache<string, string> HMACTokenLru
48+
= new(1024, TimeSpan.FromSeconds(30));
49+
4450
/// <summary>
4551
/// The function processing the Http request.
4652
/// </summary>
@@ -222,19 +228,6 @@ private async Task Invoke(HttpContext httpContext, bool retry)
222228

223229
ImageCommandContext imageCommandContext = new(httpContext, commands, this.commandParser, this.parserCulture);
224230

225-
if (doHMAC)
226-
{
227-
// Compare the passed token to our generated mac.
228-
string mac = await this.options.OnComputeHMACAsync(imageCommandContext, secret);
229-
if (mac != token)
230-
{
231-
// Throw a 401. We don't log the error to avoid attempts at log poisoning.
232-
httpContext.Response.Clear();
233-
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
234-
return;
235-
}
236-
}
237-
238231
await this.options.OnParseCommandsAsync.Invoke(imageCommandContext);
239232

240233
// Get the correct service for the request
@@ -256,6 +249,26 @@ private async Task Invoke(HttpContext httpContext, bool retry)
256249
return;
257250
}
258251

252+
// At this point we know that this is a valid image request
253+
// Check for a token if required and reject if invalid.
254+
if (doHMAC)
255+
{
256+
if (token is null)
257+
{
258+
// Throw a 401. We don't log the error to avoid attempts at log poisoning.
259+
SetUnauthorized(httpContext);
260+
return;
261+
}
262+
263+
// Compare the passed token to our generated mac.
264+
string mac = await HMACTokenLru.GetOrAddAsync(token, _ => this.options.OnComputeHMACAsync(imageCommandContext, secret));
265+
if (mac != token)
266+
{
267+
SetUnauthorized(httpContext);
268+
return;
269+
}
270+
}
271+
259272
IImageResolver sourceImageResolver = await provider.GetAsync(httpContext);
260273

261274
if (sourceImageResolver is null)
@@ -275,6 +288,13 @@ await this.ProcessRequestAsync(
275288
retry);
276289
}
277290

291+
private static void SetUnauthorized(HttpContext httpContext)
292+
{
293+
httpContext.Response.Clear();
294+
httpContext.Response.Headers.Add("WWW-Authenticate", "HMAC realm=\"" + httpContext.Request.Host + "\"");
295+
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
296+
}
297+
278298
private void StripUnknownCommands(CommandCollection commands, int startAtIndex)
279299
{
280300
var keys = new List<string>(commands.Keys);

src/ImageSharp.Web/Middleware/ImageSharpMiddlewareOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class ImageSharpMiddlewareOptions
1919
private Func<ImageCommandContext, byte[], Task<string>> onComputeHMACAsync = (context, secret) =>
2020
{
2121
string uri = CaseHandlingUriBuilder.BuildRelative(
22-
CaseHandlingUriBuilder.CaseHandling.LowerInvariant,
22+
CaseHandlingUriBuilder.CaseHandling.None,
2323
context.Context.Request.PathBase,
2424
context.Context.Request.Path,
2525
QueryString.Create(context.Commands));
@@ -85,7 +85,7 @@ public class ImageSharpMiddlewareOptions
8585

8686
/// <summary>
8787
/// Gets or sets the method used to compute a Hash-based Message Authentication Code (HMAC) for request authentication.
88-
/// Defaults to <see cref="HMACUtilities.ComputeHMACSHA256(string, byte[])"/> using an invariant lowercase relative Uri
88+
/// Defaults to <see cref="HMACUtilities.ComputeHMACSHA256(string, byte[])"/> using a relative Uri
8989
/// generated using <see cref="CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling, PathString, PathString, QueryString)"/>.
9090
/// </summary>
9191
public Func<ImageCommandContext, byte[], Task<string>> OnComputeHMACAsync

tests/ImageSharp.Web.Tests/TestUtilities/AuthenticatedServerTestBase.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Xunit;
48
using Xunit.Abstractions;
59

610
namespace SixLabors.ImageSharp.Web.Tests.TestUtilities
@@ -13,10 +17,30 @@ protected AuthenticatedServerTestBase(TFixture fixture, ITestOutputHelper output
1317
{
1418
}
1519

20+
[Fact]
21+
public async Task CanRejectUnauthorizedRequestAsync()
22+
{
23+
string url = this.ImageSource;
24+
25+
// Send an unaugmented request without a token.
26+
HttpResponseMessage response = await this.HttpClient.GetAsync(url + this.Fixture.Commands[0]);
27+
Assert.NotNull(response);
28+
Assert.False(response.IsSuccessStatusCode);
29+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
30+
Assert.True(response.Headers.Contains("WWW-Authenticate"));
31+
32+
// Now send an invalid token
33+
response = await this.HttpClient.GetAsync(url + this.Fixture.Commands[0] + "&" + HMACUtilities.TokenCommand + "=INVALID");
34+
Assert.NotNull(response);
35+
Assert.False(response.IsSuccessStatusCode);
36+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
37+
Assert.True(response.Headers.Contains("WWW-Authenticate"));
38+
}
39+
1640
protected override string AugmentCommand(string command)
1741
{
18-
// Mimic the lowecase relative url format used by the token and default options.
19-
string uri = (this.ImageSource + command).Replace("http://localhost", string.Empty).ToLowerInvariant();
42+
// Mimic the case sensitive url format used by the token and default options.
43+
string uri = (this.ImageSource + command).Replace("http://localhost", string.Empty);
2044
string token = HMACUtilities.ComputeHMACSHA256(uri, AuthenticatedTestServerFixture.HMACSecretKey);
2145
return command + "&" + HMACUtilities.TokenCommand + "=" + token;
2246
}

0 commit comments

Comments
 (0)