one actor's data, served to another
Cross-Boundary State Exposure
A singleton, module global, or pooled object is reused across requests, threads, or sessions without resetting its per-actor fields. Whoever touched it last leaves their data for whoever touches it next — so one user is handed another's state.
Per-actor state parked in a shared singleton, global, or pool bleeds across the boundary that was meant to keep actors apart.
In the wild
Shared Singleton Holds Per-Request State
A single shared object with mutable, per-call state lets concurrent actors overwrite and read each other's data.
// SMELL: one shared SimpleDateFormat for every thread
static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd");
String render(Date d) { return FMT.format(d); } // FMT keeps a mutable
// Calendar -> threads read
// each other's half-write
// RIGHT: give each call its own formatter (no shared state)
String render(Date d) {
return new SimpleDateFormat("yyyy-MM-dd").format(d);
}shared: garbled / interleaved dates under load per-call: every thread formats its own date correctly
# SMELL: a module-level slot reused by every concurrent request
_current_user = None
def set_user(u):
global _current_user
_current_user = u
def audit(action):
log(_current_user, action) # another request may have overwritten it
# RIGHT: carry per-request state with the request, not in a global
def audit(user, action):
log(user, action) # state travels with the callerglobal: request B's action logged under user A per-call: each request keeps its own user
Pooled Object Reused Without Reset
An object returned to a pool still carrying its last user's fields hands that data to the next borrower.
# SMELL: a pooled context handed out without clearing the last user
ctx = pool.acquire() # still holds the previous request's user
ctx.run(request) # ...so this request acts as the wrong user
pool.release(ctx)
# RIGHT: reset the per-actor state on borrow, so nothing crosses over
ctx = pool.acquire()
ctx.reset() # wipe identity before reuse
ctx.user = request.user
ctx.run(request)
pool.release(ctx)no reset: request acts under the prior user's identity reset: each borrow starts from a clean object
// SMELL: a ThreadLocal left set when the thread returns to the pool
static final ThreadLocal<User> CURRENT = new ThreadLocal<>();
void handle(Req r) {
CURRENT.set(r.user());
process(); // pooled thread reused -> CURRENT still set
} // for the *next* request on this thread
// RIGHT: clear the ThreadLocal in a finally, before the thread is recycled
void handle(Req r) {
CURRENT.set(r.user());
try { process(); }
finally { CURRENT.remove(); }
}leak: next request inherits the prior user via ThreadLocal remove: state cleared before the thread is reused
Where this compounds
- Concurrent Singleton Mutation × this Mutation
- Cross-Session Contamination × Lack of Input Validation
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.