fix(SEC-38,SEC-39): preserve aggregate and fallback throttles
This commit is contained in:
@@ -28,8 +28,11 @@ namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
||||
/// <para>
|
||||
/// The tracked partition set is a bounded LRU (<see cref="SecurityOptions.ApiKeyFailureTrackedPeers"/>).
|
||||
/// Because only a validly shaped token mints a key-id partition and each transport peer may mint at
|
||||
/// most <see cref="MaxKeyIdPartitionsPerPeer"/> of them (the overflow collapses onto that address's
|
||||
/// fallback partition), the map bounds memory <em>and</em> resists a junk-token flush (SEC-32):
|
||||
/// most <see cref="MaxKeyIdPartitionsPerPeer"/> of them (the peer-layer overflow collapses onto that
|
||||
/// address's fallback partition), the map bounds memory <em>and</em> resists a junk-token flush
|
||||
/// (SEC-32). A validly shaped presented key id still feeds its cross-peer aggregate when its peer
|
||||
/// layer uses the fallback, so exhausting the per-peer cap cannot bypass distributed-spray
|
||||
/// protection; the aggregate map remains bounded by the same capped eviction policy:
|
||||
/// eviction prefers fully expired windows, never removes an over-limit partition below a transient
|
||||
/// overshoot ceiling of twice the cap, and only then falls back to least-recently-active. Successful
|
||||
/// peers are removed on reset, so in steady state the map holds only partitions with recent failures
|
||||
@@ -155,9 +158,9 @@ public sealed class ApiKeyFailureLimiter
|
||||
|
||||
WindowState? peerState = _partitions.TryGetValue(partitionKey, out WindowState? tracked) ? tracked : null;
|
||||
WindowState? aggregateState = null;
|
||||
if (effectiveKeyId is not null
|
||||
if (partition.KeyId is not null
|
||||
&& _aggregateLimit > 0
|
||||
&& _aggregates.TryGetValue(effectiveKeyId, out WindowState? aggregate))
|
||||
&& _aggregates.TryGetValue(partition.KeyId, out WindowState? aggregate))
|
||||
{
|
||||
aggregateState = aggregate;
|
||||
}
|
||||
@@ -222,11 +225,13 @@ public sealed class ApiKeyFailureLimiter
|
||||
(Peer: peer, KeyId: effectiveKeyId));
|
||||
RecordInto(state, now, _limit);
|
||||
|
||||
// Only a key id that earned its own partition feeds the aggregate: an id squeezed out by the
|
||||
// per-peer cap is spray, and letting it through would make aggregate cardinality unbounded.
|
||||
if (effectiveKeyId is not null && _aggregateLimit > 0)
|
||||
// The aggregate follows the validly shaped presented key id independently of the peer
|
||||
// layer's storage choice. Otherwise an attacker could exhaust each peer's dedicated-key cap
|
||||
// and route a victim id through the fallback to bypass cross-peer throttling. Aggregate
|
||||
// cardinality is still bounded by EvictIfOverCapacity below.
|
||||
if (partition.KeyId is not null && _aggregateLimit > 0)
|
||||
{
|
||||
WindowState aggregate = _aggregates.GetOrAdd(effectiveKeyId, static _ => new WindowState(null, null));
|
||||
WindowState aggregate = _aggregates.GetOrAdd(partition.KeyId, static _ => new WindowState(null, null));
|
||||
RecordInto(aggregate, now, _aggregateLimit);
|
||||
}
|
||||
|
||||
@@ -261,7 +266,8 @@ public sealed class ApiKeyFailureLimiter
|
||||
// contributed by other key ids — and by junk-shaped tokens — from the same address, so
|
||||
// removing it would let one successful authentication wipe an in-progress spray. The shared
|
||||
// bucket decays by window expiry instead; the caller still recovers through probe admission.
|
||||
if (string.Equals(effectiveKeyId, partition.KeyId, StringComparison.Ordinal))
|
||||
if (effectiveKeyId is not null
|
||||
&& string.Equals(effectiveKeyId, partition.KeyId, StringComparison.Ordinal))
|
||||
{
|
||||
RemovePartition(partitionKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user