From a31860ee0f1cfe4510603cf9550678549994ceac Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 5 Feb 2025 15:50:37 +0000 Subject: [PATCH 1/9] additional guidance on cache key composition --- aspnetcore/performance/caching/hybrid.md | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 9cf147ad9b9e..e0cc73e9db1f 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -52,6 +52,40 @@ The stateless overload of `GetOrCreateAsync` is recommended for most scenarios. :::code language="csharp" source="~/performance/caching/hybrid/samples/9.x/HCMinimal/Program.cs" id="snippet_getorcreate" highlight="5-12"::: +### Cache key guidance + +The `key` passed to `GetOrCreateAsync` is usually formed using string concatenation, and must uniquely identify +the data being cached both in terms of the identifiers/values used to load that data, and in terms of other data +cached in your application. For example: + +```c# +cache.GetOrCreateAsync($"/orders/{region}/{orderId}", ...); +``` + +or + +```c# +cache.GetOrCreateAsync($"user_prefs_{userId}", ...); +``` + +**It is the caller's responsibility** to ensure that this key scheme is valid and cannot cause data to become confused, either between separate uses of `HybridCache`, or based on +different inputs used in the cache key. In particular, it is not recommended to use external user input (in particular, but not limited to, raw `string` values from a UI) as part +of cache keys, as this could allow malicious access attempts, or could be used in a service-denial attack by saturating your cache with data with meaningless keys generated from +random strings. In the above (valid) example, the *order* data and *user preference* data are clearly distinct, where `orderid` and `userId` are our internally generated +identifiers, and `region` might be an enum or string from a *predefined list* of known regions. There is no significance placed on tokens such as `/` or `_`; the composed value is +treated as an opaque identifying string - the examples above just illustrate some common approaches. This guidance applies equally to any `string`-based cache API, including +`HybridCache`, `IDistributedCache`, `IMemoryCache`, etc. + +The inline interpolated string syntax (`$"..."` above, directly inside the `GetOrCreateAsync` call) is recommended when using `HybridCache`, as this allows for planned future +improvements that bypass the need to allocate a `string` for the key in many scenarios. + +Additional key restrictions: + +- keys may restricted to valid lengths; for example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default, (configurable via `HybridCacheOptions.MaximumKeyLength`) with longer keys bypassing the cache mechanisms to prevent saturation. +- keys must be valid unicode sequences; if invalid unicode sequences are passed, the behaviour is undefined. +- when using an out-of-process secondary cache (`IDistributedCache`), the specific backend implementation may impose additional restrictions - as a hypothetical example (no such actual + backend is known), a particular backend might use case-insensitive key logic; the default `HybridCache` (via `AddHybridCache(...)`) will detect this scenario to prevent confusion attacks, however it may still result in conflicting keys becoming overwritten/evicted sooner than expected. + ### The alternative `GetOrCreateAsync` overload The alternative overload might reduce some overhead from [captured variables](/dotnet/csharp/language-reference/operators/lambda-expressions#capture-of-outer-variables-and-variable-scope-in-lambda-expressions) and per-instance callbacks, but at the expense of more complex code. For most scenarios the performance increase doesn't outweigh the code complexity. Here's an example that uses the alternative overload: From 0acfd65f17ef24c646cd3f0e38ce4e2d7c9ca7b8 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 16:57:58 +0000 Subject: [PATCH 2/9] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Tom Dykstra --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index e0cc73e9db1f..1705af60bdde 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -52,7 +52,7 @@ The stateless overload of `GetOrCreateAsync` is recommended for most scenarios. :::code language="csharp" source="~/performance/caching/hybrid/samples/9.x/HCMinimal/Program.cs" id="snippet_getorcreate" highlight="5-12"::: -### Cache key guidance +## Cache key guidance The `key` passed to `GetOrCreateAsync` is usually formed using string concatenation, and must uniquely identify the data being cached both in terms of the identifiers/values used to load that data, and in terms of other data From 08112547f7324e207f8ac04763291f457bd8eca3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 16:58:20 +0000 Subject: [PATCH 3/9] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Tom Dykstra --- aspnetcore/performance/caching/hybrid.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 1705af60bdde..314400a4af3f 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -54,9 +54,12 @@ The stateless overload of `GetOrCreateAsync` is recommended for most scenarios. ## Cache key guidance -The `key` passed to `GetOrCreateAsync` is usually formed using string concatenation, and must uniquely identify -the data being cached both in terms of the identifiers/values used to load that data, and in terms of other data -cached in your application. For example: +The `key` passed to `GetOrCreateAsync` must uniquely identify the data being cached: + +* In terms of the identifier values used to retrieve that data from its source. +* In terms of other data cached in the application. + +Both types of uniqueness are usually ensured by using string concatenation to make a single key string composed of different parts concatenated into one string. For example: ```c# cache.GetOrCreateAsync($"/orders/{region}/{orderId}", ...); From ff5055defbd62997cc1d619b138024869d9f7d4d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 16:59:27 +0000 Subject: [PATCH 4/9] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Tom Dykstra --- aspnetcore/performance/caching/hybrid.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 314400a4af3f..e3c4927c291b 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -71,13 +71,16 @@ or cache.GetOrCreateAsync($"user_prefs_{userId}", ...); ``` -**It is the caller's responsibility** to ensure that this key scheme is valid and cannot cause data to become confused, either between separate uses of `HybridCache`, or based on -different inputs used in the cache key. In particular, it is not recommended to use external user input (in particular, but not limited to, raw `string` values from a UI) as part -of cache keys, as this could allow malicious access attempts, or could be used in a service-denial attack by saturating your cache with data with meaningless keys generated from -random strings. In the above (valid) example, the *order* data and *user preference* data are clearly distinct, where `orderid` and `userId` are our internally generated -identifiers, and `region` might be an enum or string from a *predefined list* of known regions. There is no significance placed on tokens such as `/` or `_`; the composed value is -treated as an opaque identifying string - the examples above just illustrate some common approaches. This guidance applies equally to any `string`-based cache API, including -`HybridCache`, `IDistributedCache`, `IMemoryCache`, etc. +It's the caller's responsibility to ensure that a key scheme is valid and can't cause data to become confused. + + We recommend that you not use external user input in the cache key. For example, don't use raw `string` values from a UI as part of a cache key. Such keys could allow malicious access attempts, or could be used in a denial-of-service attack by saturating your cache with data having meaningless keys generated from random strings. In the preceding valid examples, the *order* data and *user preference* data are clearly distinct: + +* `orderid` and `userId` are internally generated identifiers. +* `region` might be an enum or string from a predefined list of known regions. + +There is no significance placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. You could omit the `/` and `_` with no change to the way the cache functions. + +This guidance applies equally to any `string`-based cache API, such as `HybridCache`, `IDistributedCache`, and `IMemoryCache`. The inline interpolated string syntax (`$"..."` above, directly inside the `GetOrCreateAsync` call) is recommended when using `HybridCache`, as this allows for planned future improvements that bypass the need to allocate a `string` for the key in many scenarios. From 5107e1de9fc0d8fd6a22f8522807652a83133576 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 17:03:49 +0000 Subject: [PATCH 5/9] explicit confused key example --- aspnetcore/performance/caching/hybrid.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index e3c4927c291b..98c7bfd3aba3 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -78,7 +78,13 @@ It's the caller's responsibility to ensure that a key scheme is valid and can't * `orderid` and `userId` are internally generated identifiers. * `region` might be an enum or string from a predefined list of known regions. -There is no significance placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. You could omit the `/` and `_` with no change to the way the cache functions. +There is no significance placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. In this case, you could omit the `/` and `_` with no +change to the way the cache functions, but a delimiter is usually used to avoid ambiguity - for example `$"order{customerId}{orderId}"` could cause confusion betwen: + +- `customerId` 42 with `orderId` 123 +- `customerId` 421 with `orderId` 23 + +(both of which would generate the cache key `order42123`) This guidance applies equally to any `string`-based cache API, such as `HybridCache`, `IDistributedCache`, and `IMemoryCache`. From 020e9a043b292649c1551272a3f3d77b7eb6d88d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 17:04:10 +0000 Subject: [PATCH 6/9] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Tom Dykstra --- aspnetcore/performance/caching/hybrid.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 98c7bfd3aba3..316f6f6a96c9 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -88,8 +88,7 @@ change to the way the cache functions, but a delimiter is usually used to avoid This guidance applies equally to any `string`-based cache API, such as `HybridCache`, `IDistributedCache`, and `IMemoryCache`. -The inline interpolated string syntax (`$"..."` above, directly inside the `GetOrCreateAsync` call) is recommended when using `HybridCache`, as this allows for planned future -improvements that bypass the need to allocate a `string` for the key in many scenarios. +Notice that the inline interpolated string syntax (`$"..."` in the preceding examples of valid keys) is directly inside the `GetOrCreateAsync` call. This syntax is recommended when using `HybridCache`, as it allows for planned future improvements that bypass the need to allocate a `string` for the key in many scenarios. Additional key restrictions: From 9fd7436c75f668720ead7c0ffc91019c4c80483c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 17:04:43 +0000 Subject: [PATCH 7/9] Update aspnetcore/performance/caching/hybrid.md Co-authored-by: Tom Dykstra --- aspnetcore/performance/caching/hybrid.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 316f6f6a96c9..7548b83de5e4 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -90,12 +90,11 @@ This guidance applies equally to any `string`-based cache API, such as `HybridCa Notice that the inline interpolated string syntax (`$"..."` in the preceding examples of valid keys) is directly inside the `GetOrCreateAsync` call. This syntax is recommended when using `HybridCache`, as it allows for planned future improvements that bypass the need to allocate a `string` for the key in many scenarios. -Additional key restrictions: +### Additional key considerations -- keys may restricted to valid lengths; for example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default, (configurable via `HybridCacheOptions.MaximumKeyLength`) with longer keys bypassing the cache mechanisms to prevent saturation. -- keys must be valid unicode sequences; if invalid unicode sequences are passed, the behaviour is undefined. -- when using an out-of-process secondary cache (`IDistributedCache`), the specific backend implementation may impose additional restrictions - as a hypothetical example (no such actual - backend is known), a particular backend might use case-insensitive key logic; the default `HybridCache` (via `AddHybridCache(...)`) will detect this scenario to prevent confusion attacks, however it may still result in conflicting keys becoming overwritten/evicted sooner than expected. +* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`), with longer keys bypassing the cache mechanisms to prevent saturation. +* Keys must be valid Unicode sequences. If invalid Unicode sequences are passed, the behavior is undefined. +- When using an out-of-process secondary cache such as `IDistributedCache`, the specific backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks, however it may still result in conflicting keys becoming overwritten or evicted sooner than expected. ### The alternative `GetOrCreateAsync` overload From 708f692d294d26f9910d7ae834af734af3ac2076 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Feb 2025 17:06:12 +0000 Subject: [PATCH 8/9] unbalanced paren --- aspnetcore/performance/caching/hybrid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 7548b83de5e4..93cb2caeda86 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -92,9 +92,9 @@ Notice that the inline interpolated string syntax (`$"..."` in the preceding exa ### Additional key considerations -* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`), with longer keys bypassing the cache mechanisms to prevent saturation. +* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`, with longer keys bypassing the cache mechanisms to prevent saturation. * Keys must be valid Unicode sequences. If invalid Unicode sequences are passed, the behavior is undefined. -- When using an out-of-process secondary cache such as `IDistributedCache`, the specific backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks, however it may still result in conflicting keys becoming overwritten or evicted sooner than expected. +* When using an out-of-process secondary cache such as `IDistributedCache`, the specific backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks, however it may still result in conflicting keys becoming overwritten or evicted sooner than expected. ### The alternative `GetOrCreateAsync` overload From 706798ef946e6f2159c868a438f5debccac0d831 Mon Sep 17 00:00:00 2001 From: Tom Dykstra Date: Thu, 6 Feb 2025 11:12:00 -0800 Subject: [PATCH 9/9] betwen --> between --- aspnetcore/performance/caching/hybrid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/performance/caching/hybrid.md b/aspnetcore/performance/caching/hybrid.md index 93cb2caeda86..1227f9d142a1 100644 --- a/aspnetcore/performance/caching/hybrid.md +++ b/aspnetcore/performance/caching/hybrid.md @@ -79,7 +79,7 @@ It's the caller's responsibility to ensure that a key scheme is valid and can't * `region` might be an enum or string from a predefined list of known regions. There is no significance placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. In this case, you could omit the `/` and `_` with no -change to the way the cache functions, but a delimiter is usually used to avoid ambiguity - for example `$"order{customerId}{orderId}"` could cause confusion betwen: +change to the way the cache functions, but a delimiter is usually used to avoid ambiguity - for example `$"order{customerId}{orderId}"` could cause confusion between: - `customerId` 42 with `orderId` 123 - `customerId` 421 with `orderId` 23