Skip to content

Conversation

@ronaldbarendse
Copy link
Contributor

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

As mentioned in https://github.com/SixLabors/ImageSharp.Web/pull/250/files#r857755086, caching generated HMACs based on the token completely bypasses the validation, because the cache key doesn't change when the commands are changed and therefore returns the same valid HMAC when the token is used with different commands.

I've thought about creating a better key for the HMAC cache, but that would also require additional processing and you might get similar issues when that cache key doesn't use the same information as OnComputeHMACAsync does (e.g. the HMAC cache key uses a relative URL, but the HMAC is computed using an absolute URL).

The overhead of generating HMACs is also relatively low and attackers could otherwise just change the URL slightly to bypass the cached HMACs anyway. This is why I think completely removing the HMAC cache seems like the most reasonable fix.


You can quickly test this by using the following code and appending &hmac= with the logged value:

using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Extensions;

services.AddImageSharp(options =>
{
    // Randomly generate a HMAC secret key
    byte[] secret = new byte[64];
    RandomNumberGenerator.Create().GetBytes(secret);
    options.HMACSecretKey = secret;

    // Log computed HMAC to console for testing
    var onComputeHMACAsync = options.OnComputeHMACAsync;
    options.OnComputeHMACAsync = async (context, secret) =>
    {
        var hmac = await onComputeHMACAsync(context, secret);

        Console.WriteLine($"Computed HMAC for {context.Context.Request.GetDisplayUrl()}: {hmac}");

        return hmac;
    };
});

@codecov
Copy link

codecov bot commented Apr 25, 2022

Codecov Report

Merging #256 (96189ec) into main (ba216da) will decrease coverage by 0%.
The diff coverage is 100%.

@@         Coverage Diff         @@
##           main   #256   +/-   ##
===================================
- Coverage    85%    85%   -1%     
===================================
  Files        74     74           
  Lines      2028   2025    -3     
  Branches    297    296    -1     
===================================
- Hits       1734   1730    -4     
- Misses      210    212    +2     
+ Partials     84     83    -1     
Flag Coverage Δ
unittests 85% <100%> (-1%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
.../ImageSharp.Web/Middleware/ImageSharpMiddleware.cs 81% <100%> (+<1%) ⬆️
.../Synchronization/RefCountedConcurrentDictionary.cs 72% <0%> (-4%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ba216da...96189ec. Read the comment docs.

@JimBobSquarePants
Copy link
Member

What are you on about?

Can you please raise an issue proving a bug in the implementation. There is protocol here and you’re not following it and adding unnecessary work for me chasing down random comments littered though the development history.

@ronaldbarendse
Copy link
Contributor Author

What are you on about?

I thought this was just too obvious if you look at this single line of code:

hmac = await HMACTokenLru.GetOrAddAsync(token, _ => this.options.OnComputeHMACAsync(imageCommandContext, secret));

The HMAC is generated based on the information in the ImageCommandContext (by default the relative path and parsed commands), but then cached using only the token (a single command from the request). Any subsequent request using the same token will therefore return the same valid HMAC from the cache, even if you've supplied different commands in that request!

@JimBobSquarePants
Copy link
Member

JimBobSquarePants commented May 18, 2022

Wouldn't the fix then be as simple as using the full encoded (or even display) URL as the key?

Although the overhead of that might well negate the benefit of caching.

[InlineData("Over the lazy dog.", "2ea9f37afac5ae93e27d835cc6061ad84b31064ae29f451c9136e88ad5686a1e")]
public void CanGenerate256BitHMAC(string value, string expected)
{
string expected = HMACUtilities.ComputeHMACSHA256(value, HMACSecretKey);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't understand why you changed these tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because checking Assert.Equal(expected, actual); doesn't make sense if they're both generated exactly the same way...

The only valuable assertion is whether the length is correct (okay and that it doesn't throw exceptions), but if it would generate a static string of the correct length, this test will still succeed!

// First check for a HMAC token and capture before the command is stripped out.
byte[] secret = this.options.HMACSecretKey;
bool checkHMAC = false;
bool computeHMAC = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the variable name here incorrectly changes the meaning of the bool field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because the meaning has changed! Instead of always requiring a valid token when a secret is configured, it's dependent on whether the OnComputeHMACAsync returns null or not.

Together with removing the caching, this allows you to bypass the HMAC check based on the request, e.g. whether a user is logged in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants