@@ -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>
@@ -191,9 +197,18 @@ public ImageSharpMiddleware(
191197
192198 private async Task Invoke ( HttpContext httpContext , bool retry )
193199 {
194- // We expect to get concrete collection type which removes virtual dispatch concerns and enumerator allocations
195200 CommandCollection commands = this . requestParser . ParseRequestCommands ( httpContext ) ;
196201
202+ // First check for a HMAC token and capture before the command is stripped out.
203+ byte [ ] secret = this . options . HMACSecretKey ;
204+ bool checkHMAC = false ;
205+ string token = null ;
206+ if ( secret ? . Length > 0 )
207+ {
208+ checkHMAC = true ;
209+ token = commands . GetValueOrDefault ( HMACUtilities . TokenCommand ) ;
210+ }
211+
197212 if ( commands . Count > 0 )
198213 {
199214 // Strip out any unknown commands, if needed.
@@ -203,18 +218,15 @@ private async Task Invoke(HttpContext httpContext, bool retry)
203218 if ( ! this . knownCommands . Contains ( command ) )
204219 {
205220 // Need to actually remove, allocates new list to allow modifications
206- this . StripUnknownCommands ( commands , startAtIndex : index ) ;
221+ this . StripUnknownCommands ( commands , index ) ;
207222 break ;
208223 }
209224
210225 index ++ ;
211226 }
212227 }
213228
214- await this . options . OnParseCommandsAsync . Invoke (
215- new ImageCommandContext ( httpContext , commands , this . commandParser , this . parserCulture ) ) ;
216-
217- // Get the correct service for the request
229+ // Get the correct provider for the request
218230 IImageProvider provider = null ;
219231 foreach ( IImageProvider resolver in this . providers )
220232 {
@@ -225,14 +237,48 @@ await this.options.OnParseCommandsAsync.Invoke(
225237 }
226238 }
227239
228- if ( ( commands . Count == 0 && provider ? . ProcessingBehavior != ProcessingBehavior . All )
229- || provider ? . IsValidRequest ( httpContext ) != true )
240+ if ( provider ? . IsValidRequest ( httpContext ) != true )
241+ {
242+ // Nothing to do. call the next delegate/middleware in the pipeline
243+ await this . next ( httpContext ) ;
244+ return ;
245+ }
246+
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 )
252+ {
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+ }
261+
262+ await this . options . OnParseCommandsAsync . Invoke ( imageCommandContext ) ;
263+
264+ if ( commands . Count == 0 && provider ? . ProcessingBehavior != ProcessingBehavior . All )
230265 {
231266 // Nothing to do. call the next delegate/middleware in the pipeline
232267 await this . next ( httpContext ) ;
233268 return ;
234269 }
235270
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 )
276+ {
277+ SetBadRequest ( httpContext ) ;
278+ return ;
279+ }
280+ }
281+
236282 IImageResolver sourceImageResolver = await provider . GetAsync ( httpContext ) ;
237283
238284 if ( sourceImageResolver is null )
@@ -252,6 +298,14 @@ await this.ProcessRequestAsync(
252298 retry ) ;
253299 }
254300
301+ private static void SetBadRequest ( HttpContext httpContext )
302+ {
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.
305+ httpContext . Response . Clear ( ) ;
306+ httpContext . Response . StatusCode = StatusCodes . Status400BadRequest ;
307+ }
308+
255309 private void StripUnknownCommands ( CommandCollection commands , int startAtIndex )
256310 {
257311 var keys = new List < string > ( commands . Keys ) ;
0 commit comments