Shared Mutable State
ID |
kotlin.shared_mutable_state |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Synchronization |
Language |
Kotlin |
Tags |
ASVS50:v15.4.1, CWE:362, CWE:567, NIST.SP.800-53, PCI-DSS:6.5.6 |
Description
Detects shared mutable state — properties of an object, of a companion object, of the file itself, or of a singleton bean — 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 property is assigned a value read from a function parameter, concurrent requests hand one caller’s data to another, and that finding is raised to high. When the property is a counter or a mutable collection written concurrently, updates are lost and the collection can corrupt.
One issue is reported per property, at its worst write.
Rationale
A Kotlin object and a companion object exist once per process, and so does a property declared at the top level of a file. Spring components annotated with @Service, @Component, @Repository or @RestController are instantiated once per application and shared across all requests. Every concurrent request thread reads and writes the same property.
A property holding request data:
@Service
class UserService {
private var currentUser: String? = null // Shared across all requests
fun handleRequest(userId: String) {
currentUser = userId
notifyUser(currentUser) // May be a different user due to a concurrent request
}
}
A registry in an object, written on every call:
object HitRegistry {
private val hits = HashMap<String, Int>()
fun record(page: String) {
hits[page] = 1 // HashMap is not safe for concurrent writes
}
}
Remediation
-
Eliminate Shared State: Move request-specific data to function parameters, return values, or request-scoped beans (
@RequestScope). -
Use Thread-Safe Types: For state that truly lives at the application level, use
AtomicInteger,AtomicLongorLongAdderfor counters andConcurrentHashMapfor caches. -
Synchronize Access: If shared state is unavoidable, guard every access with the same lock:
private val lock = ReentrantLock() private val cache = HashMap<String, String>() fun updateCache(key: String, value: String) = lock.withLock { cache[key] = value }
Not Reported
-
A thread-safe type (
ConcurrentHashMap,AtomicLong,ThreadLocal…), declared or constructed, or a value built by a synchronizing factory (Collections.synchronizedMap). -
A write inside
synchronized(…) { },lock.withLock { }orwithPermit { }, in a@Synchronizedfunction, or after alock()on a lock property. -
A bean with a request, session or prototype scope (
@RequestScope,@Scope("request")). -
An injected property (
@Autowired,@Value,@Inject…) and a delegated one (by lazy { … }). -
A lazy initialisation,
if (x == null) x = …, whose value is not a function parameter. -
A write in an
initblock, a constructor, a property initializer,@PostConstruct,afterPropertiesSet, a start-up@EventListener, or a@ConfigurationPropertiesclass. -
An injection point: an
@Autowiredor@Valuefunction, a container callback such assetApplicationContext, or a plain property setter that is not a request handler. -
A helper that only initialization calls, such as a registration function called from an
initblock. -
A call named like a mutator on a property that is not a mutable collection (
redis.set(…),NAME.replace(…)). -
A test source.