Shared Mutable State
ID |
java.shared_mutable_state |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Synchronization |
Language |
Java |
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 or fields of singleton beans — 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, concurrent requests hand one caller’s data to another, and that finding is raised to high. When the field is a counter or plain collection written concurrently, updates are lost and the collection can corrupt.
One issue is reported per field, at its worst write.
A field that mirrors an application setting kept in a store is not reported: it is loaded while a flag says the load has not run yet, and every other write is followed by saving the value. The store already shares that value with every caller.
Rationale
In multi-threaded applications, particularly those using dependency injection frameworks like Spring, shared mutable state that is modified outside synchronization or thread-safe types creates data races.
Servlets handle multiple HTTP requests concurrently. Spring components annotated with @Service, @Component, or @Repository are instantiated once per application and shared across all requests. Instance fields of these singletons are accessible from multiple concurrent request threads.
Consider the following examples:
A field holding request data:
@Service
public class UserService {
private String currentUser; // Shared across all requests
public void handleRequest(String userId) {
currentUser = userId;
// ... later, in another method called from same request:
notifyUser(currentUser); // May be a different user due to concurrent request
}
}
A counter that loses updates:
@Service
public class RequestCounter {
private int count = 0; // No synchronization
public void increment() {
count++; // Race condition: read-modify-write is not atomic
}
public int getCount() {
return count;
}
}
Remediation
To fix shared mutable state issues:
-
Eliminate Shared State: Move request-specific data to method parameters, return values, or request-scoped beans. Use
@Scope(WebApplicationContext.SCOPE_REQUEST)or@RequestScopefor beans that hold per-request data. -
Use Thread-Safe Types: For shared state that truly must live at the application level (caches, shared counters), use thread-safe classes:
-
AtomicInteger,AtomicLongfor counters -
ConcurrentHashMapfor caches -
LongAdderorDoubleAdderfor high-contention counters
-
-
Synchronize Access: If shared state is unavoidable, guard all accesses with the same lock or synchronized block:
private final Object lock = new Object(); private Map<String, String> cache; public void updateCache(String key, String value) { synchronized (lock) { cache.put(key, value); } }
Not Reported
-
A thread-safe type (
ConcurrentHashMap,AtomicLong,ThreadLocal…), declared or allocated, or a value built by a synchronizing factory (Collections.synchronizedMap). -
An instance of a project class that guards its own state: every field it assigns or increments outside its constructors is written under
synchronizedor a lock, and every field it changes without one only ever holds a thread-safe value. -
A write inside
synchronized, in asynchronizedmethod, or after alock()on a lock field. -
A bean with a request, session or prototype scope (
@RequestScope,@Scope("request"),@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)). -
An injected field (
@Autowired,@Value,@Inject…). -
A lazy initialisation,
if (x == null) x = …, whose value is not a method parameter. -
A write in a constructor, an initialiser block,
@PostConstruct,afterPropertiesSet, a start-up@EventListeneroronApplicationEvent, a@ConfigurationPropertiesclass, or a shut-down callback (@PreDestroy,DisposableBean.destroy). -
A method that only start-up reaches: every call to it in the project is made by a start-up callback, a
mainmethod or a static initialiser, directly or through other methods only those reach. -
A method only tests call, and a private method nothing calls.
-
A field only administrators write, when the convention profile names the check that admits them (
admin_only): every write to it runs below that check,if (Validate.validateAdminSession(…))or after an early return on its negation, or in a method every call to which does. Such a field is a setting of the application. Once any other caller writes it too, it is reported.
A static field of another class is followed when that class is declared in the same file.