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:

  1. Eliminate Shared State: Move request-specific data to method parameters, return values, or request-scoped beans. Use @Scope(WebApplicationContext.SCOPE_REQUEST) or @RequestScope for beans that hold per-request data.

  2. Use Thread-Safe Types: For shared state that truly must live at the application level (caches, shared counters), use thread-safe classes:

    • AtomicInteger, AtomicLong for counters

    • ConcurrentHashMap for caches

    • LongAdder or DoubleAdder for high-contention counters

  3. 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 synchronized or a lock, and every field it changes without one only ever holds a thread-safe value.

  • A write inside synchronized, in a synchronized method, or after a lock() 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 @EventListener or onApplicationEvent, a @ConfigurationProperties class, 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 main method 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.

Relation to java.race_condition_servlet

While this rule is enabled, java.race_condition_servlet reports nothing and servlets are read by this rule, with the same write-based test as any other shared class. With this rule disabled, java.race_condition_servlet reports as it always has.

© 2026 Xygeni. All rights reserved