Shared Mutable State
ID |
csharp.shared_mutable_state |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Synchronization |
Language |
CSharp |
Tags |
ASVS50:v15.4.1, CWE:362, CWE:567, NIST.SP.800-53, PCI-DSS:6.5.6 |
Description
Detects shared mutable state — static fields and properties, and fields of a class the application registers as a singleton — that are written outside initialization code. State that lives once per process is accessed and modified by concurrent requests, creating potential race conditions.
When a field is assigned a value read from a method parameter (or a setter’s value), concurrent requests hand one caller’s data to another, and that finding is raised to high. When the field is a counter or a collection written concurrently, updates are lost and the collection can corrupt.
One issue is reported per field, at its worst write.
Rationale
ASP.NET Core creates a service registered with AddSingleton<T>(), AddSingleton<TService, TImplementation>() or AddHostedService<T>() once, and hands the same instance to every request. A static field is shared the same way. Controllers, and services registered with AddScoped or AddTransient, are created per request and are not reported.
A counter that loses updates:
public class HitCounter // services.AddSingleton<HitCounter>()
{
private int _served;
public int Serve()
{
_served++; // read-modify-write is not atomic
return _served;
}
}
A dictionary written by every request:
public class InMemoryOrderStore : IOrderStore // services.AddSingleton<IOrderStore, InMemoryOrderStore>()
{
private readonly Dictionary<string, Order> _orders = new();
public void Save(Order order) => _orders[order.Number] = order; // Dictionary is not safe for concurrent writes
}
Remediation
-
Eliminate Shared State: keep request data in parameters and return values, or in a scoped service.
-
Use Thread-Safe Types: counters updated through
Interlocked.Increment,ConcurrentDictionaryfor caches. -
Synchronize Access: guard every access with the same lock:
private readonly object _gate = new(); private readonly Dictionary<string, string> _cache = new(); public void Update(string key, string value) { lock (_gate) { _cache[key] = value; } }
Not Reported
-
A thread-safe type (
ConcurrentDictionary,ImmutableList,Lazy<T>,ThreadLocal<T>…), declared or constructed, and a[ThreadStatic]field. -
A write inside
lock (…) { }, in a[MethodImpl(MethodImplOptions.Synchronized)]method, or after a lock is taken on a field (await _semaphore.WaitAsync(),Monitor.Enter); anInterlockedupdate. -
An instance field of a controller, or of a service that is not registered as a singleton.
-
A lazy initialisation,
if (x == null) x = …orx ??= …, whose value is not a parameter. -
A write in a constructor, a static constructor, an
initaccessor or a host start-up callback (IHostedService.StartAsync), and in the loop of aBackgroundService.ExecuteAsync, which the host starts once, or a private helper only that loop calls. -
A member whose only write drops a cached object (
_cachedRoot = null): one reference write, after which the next reader rebuilds it. -
An options class the application binds with
Configure<T>. -
A private helper that only constructors call.
-
A call named like a mutator on a field that is not a collection (
_cache.Set(…),KeyFormat.Replace(…)). -
A test source.