ThreadLocal: correcte idioom, verkeerde tijd
Java EE Servlet-containers, circa 1999: één thread per request. Een thread behandelt exact één request van begin tot eind, en eindigt daarna. ThreadLocal slaat een waarde op met de huidige thread als sleutel. Bij one-thread-per-request behoort een waarde in ThreadLocal tot precies één request. Het idioom: correct.
Threadpools veranderden het contract. Een thread behandelt request A, slaat principal A op in ThreadLocal, voltooit request A en keert terug naar de pool. Threadpools resetten de thread-status niet. ThreadLocal.remove() ruimt op, maar vereist expliciete discipline. Bij falende discipline draait request B op dezelfde thread en leest principal A uit ThreadLocal.
De 5-stapslek:
1. Request A arriveert. Server wijst Thread-7 toe.
2. Thread-7 roept ThreadLocal.set(principal_A) aan bij het begin van het verzoek.
3. Verzoek A is voltooid. Thread-7 keert terug naar de pool. ThreadLocal.remove() is niet aangeroepen.
4. Verzoek B arriveert. De server wijst Thread-7 toe (hergebruik van de pool).
5. Thread-7 leest ThreadLocal.get(): retourneert principal_A. Verzoek B draait onder de verkeerde identiteit.
Waarom tests dit missen
Unit tests draaien in isolatie: geen threadpool, geen hergebruik. Integratietests gebruiken verse threads of resetten de status tussen tests. Load tests warmen op met correcte gebruikers en lage concurrentie. Het defect treedt alleen op bij hergebruik van de threadpool met overlappende verzoeken, een situatie die in productie onder normale belasting voorkomt, maar niet in testconfiguraties die hierop controleren.
De beveiligingsconsequentie
De principal van gebruiker A lekt door in het verzoek van gebruiker B. Geen crash. Geen uitzondering. Een stille schending van de beveiligingsgrens: gebruiker B voert acties uit als gebruiker A, leest gegevens van gebruiker A of omzeilt de rechten van gebruiker B. Het systeem produceert geen fout. Logs tonen dat verzoek B geautoriseerd was. Alles lijkt correct.
De Vijf Stappen
De vijf stappen van een ThreadLocal-lek zijn precies van belang: het defect treedt niet op op het moment dat de verkeerde code draait. Het treedt eerder op, bij het ontbreken van een opruimstap.
Scope-Gekoppelde Waarden
ThreadLocal koppelt een waarde aan een thread. Een thread leeft langer dan een request. Mismatch.
Scope-attached values koppelen een waarde aan een unit of work. Wanneer de unit of work eindigt, eindigt de waarde mee. Geen expliciete cleanup. Geen remove() om te vergeten.
Java 21: ScopedValue
// ThreadLocal (DEFECT carrier)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal); // set at request start
// ... request handling ...
PRINCIPAL.remove(); // MOET worden aangeroepen; vaak vergeten
// ScopedValue (CORRECTE drager)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
// ... afhandeling van verzoek ...
// waarde automatisch verdwenen wanneer run() terugkeert
});
Go: context.Context
// context.Context draagt waarden expliciet over; scope = functie-aanroepketen
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx) // ctx expliciet doorgegeven; verdwijnt wanneer functie terugkeert
Python asyncio: contextvars.ContextVar
# ContextVar gekoppeld aan elke async taak
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal) # alleen voor deze taak ingesteld
# ... taakverwerking ...
PRINCIPAL.reset(token) # of: scope eindigt met de taak
De eigenschap die ze delen: de levensduur komt overeen met de werkeenheid. Wanneer het verzoek eindigt (de run() retourneert, de functie retourneert, de taak voltooid is), eindigt de waarde. Geen opruiming die je kunt vergeten. Geen pool die je kunt corrumperen.
Identificeer & Vervang
Een Java EE-applicatie slaat de tenant-ID op in een ThreadLocal bij het begin van een request. Onder hoge belasting verschijnt tenant A's ID in requests van tenant B. De queries van tenant B retourneren data van tenant A. Er wordt geen exception gegooid. Het defect treedt alleen op tijdens productieloadtests.