Skip to content

Commit 026b776

Browse files
Fix validation timing
1 parent 2394539 commit 026b776

2 files changed

Lines changed: 39 additions & 30 deletions

File tree

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,11 @@ private async Task Invoke(HttpContext httpContext, bool retry)
201201

202202
// First check for a HMAC token and capture before the command is stripped out.
203203
byte[] secret = this.options.HMACSecretKey;
204-
bool doHMAC = false;
204+
bool checkHMAC = false;
205205
string token = null;
206206
if (secret?.Length > 0)
207207
{
208-
doHMAC = true;
208+
checkHMAC = true;
209209
token = commands.GetValueOrDefault(HMACUtilities.TokenCommand);
210210
}
211211

@@ -218,19 +218,15 @@ private async Task Invoke(HttpContext httpContext, bool retry)
218218
if (!this.knownCommands.Contains(command))
219219
{
220220
// Need to actually remove, allocates new list to allow modifications
221-
this.StripUnknownCommands(commands, startAtIndex: index);
221+
this.StripUnknownCommands(commands, index);
222222
break;
223223
}
224224

225225
index++;
226226
}
227227
}
228228

229-
ImageCommandContext imageCommandContext = new(httpContext, commands, this.commandParser, this.parserCulture);
230-
231-
await this.options.OnParseCommandsAsync.Invoke(imageCommandContext);
232-
233-
// Get the correct service for the request
229+
// Get the correct provider for the request
234230
IImageProvider provider = null;
235231
foreach (IImageProvider resolver in this.providers)
236232
{
@@ -241,30 +237,44 @@ private async Task Invoke(HttpContext httpContext, bool retry)
241237
}
242238
}
243239

244-
if ((commands.Count == 0 && provider?.ProcessingBehavior != ProcessingBehavior.All)
245-
|| provider?.IsValidRequest(httpContext) != true)
240+
if (provider?.IsValidRequest(httpContext) != true)
246241
{
247242
// Nothing to do. call the next delegate/middleware in the pipeline
248243
await this.next(httpContext);
249244
return;
250245
}
251246

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)
247+
ImageCommandContext imageCommandContext = new(httpContext, commands, this.commandParser, this.parserCulture);
248+
249+
// At this point we know that this is an image request so should attempt to compute a validating HMAC..
250+
string hmac = null;
251+
if (checkHMAC && token != null)
255252
{
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-
}
253+
// Generate and cache a HMAC to validate against based upon the current valid commands from the request.
254+
//
255+
// If the command collection differs following the stripping of invalid commands prior to this point then this will mean
256+
// the token will not match our validating HMAC, however, this would be indicative of an attack and should be treated as such.
257+
//
258+
// As a rule all image requests should contain valid commands only.
259+
hmac = await HMACTokenLru.GetOrAddAsync(token, _ => this.options.OnComputeHMACAsync(imageCommandContext, secret));
260+
}
262261

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)
262+
await this.options.OnParseCommandsAsync.Invoke(imageCommandContext);
263+
264+
if (commands.Count == 0 && provider?.ProcessingBehavior != ProcessingBehavior.All)
265+
{
266+
// Nothing to do. call the next delegate/middleware in the pipeline
267+
await this.next(httpContext);
268+
return;
269+
}
270+
271+
// At this point we know that this is an image request designed for processing via this middleware.
272+
// Check for a token if required and reject if invalid.
273+
if (checkHMAC)
274+
{
275+
if (token == null || hmac != token)
266276
{
267-
SetUnauthorized(httpContext);
277+
SetBadRequest(httpContext);
268278
return;
269279
}
270280
}
@@ -288,11 +298,12 @@ await this.ProcessRequestAsync(
288298
retry);
289299
}
290300

291-
private static void SetUnauthorized(HttpContext httpContext)
301+
private static void SetBadRequest(HttpContext httpContext)
292302
{
303+
// We return a 400 rather than a 401 as we do not want to prompt follow up requests.
304+
// We don't log the error to avoid attempts at log poisoning.
293305
httpContext.Response.Clear();
294-
httpContext.Response.Headers.Add("WWW-Authenticate", "HMAC realm=\"" + httpContext.Request.Host + "\"");
295-
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
306+
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
296307
}
297308

298309
private void StripUnknownCommands(CommandCollection commands, int startAtIndex)

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ public async Task CanRejectUnauthorizedRequestAsync()
2626
HttpResponseMessage response = await this.HttpClient.GetAsync(url + this.Fixture.Commands[0]);
2727
Assert.NotNull(response);
2828
Assert.False(response.IsSuccessStatusCode);
29-
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
30-
Assert.True(response.Headers.Contains("WWW-Authenticate"));
29+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
3130

3231
// Now send an invalid token
3332
response = await this.HttpClient.GetAsync(url + this.Fixture.Commands[0] + "&" + HMACUtilities.TokenCommand + "=INVALID");
3433
Assert.NotNull(response);
3534
Assert.False(response.IsSuccessStatusCode);
36-
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
37-
Assert.True(response.Headers.Contains("WWW-Authenticate"));
35+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
3836
}
3937

4038
protected override string AugmentCommand(string command)

0 commit comments

Comments
 (0)