Shared Mutable State
ID |
python.shared_mutable_state |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Synchronization |
Language |
Python |
Tags |
ASVS50:v15.4.1, CWE:362, CWE:567, NIST.SP.800-53, PCI-DSS:6.5.6 |
Description
Detects shared mutable state — module-level globals, class attributes, and attributes of an instance a module builds at import — that is written outside initialization code. A module is imported once per process, so this state is read and written by every request a threaded server (a WSGI server, FastAPI’s threadpool for sync endpoints) runs at the same time, creating potential race conditions.
When the state 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 it is a counter or a collection written concurrently, updates are lost.
One issue is reported per piece of state, at its worst write.
Rationale
A module-level dictionary written by a view:
HITS = {}
def record_hit(request, page):
HITS[page] = 1 # every concurrent request writes the same dict
A counter rebound through global:
_served = 0
def serve(request):
global _served
_served += 1 # read-modify-write is not atomic
An instance built once at import, written per request:
class OrderStore:
def __init__(self):
self.orders = {}
def save(self, number, order):
self.orders[number] = order
order_store = OrderStore() # one instance for the whole process
Remediation
-
Eliminate Shared State: keep request data in arguments and return values, in the request, or in a
contextvars.ContextVar. -
Use Thread-Safe Types:
queue.Queueandcollections.dequefor queues,threading.localfor per-thread values, a cache library for caches. -
Synchronize Access: guard every access with the same lock:
_lock = threading.Lock() HITS = {} def record_hit(request, page): with _lock: HITS[page] = 1
Not Reported
-
A thread-safe value (
queue.Queue,collections.deque,threading.local,ContextVar, a lock). -
A write inside
witha lock the module, the class or the instance holds, or after itsacquire(). -
A lazy initialization,
if x is None: x = …, whose value is not a parameter. -
A write at import time, in
initand the other initialization methods, inAppConfig.ready(), an application factory (create_app), a lifespan, or a startup hook (@app.on_event("startup")). -
A helper that only initialization calls, such as a registration method called from
init. -
A call named like a mutator on a value that is not a collection (
cache.set(…),KEY.replace(…)), and an instance a request builds for itself. -
A function memoized with
functools.lru_cache: the cache belongs to the decorator. -
A test source.