🛡️ CVE-2026-32887 — effect
Description
Effect AsyncLocalStorage context lost/contaminated inside Effect fibers under concurrent load with RPC
Versions
effect: 3.19.15@effect/rpc: 0.72.1@effect/platform: 0.94.2- Node.js: v22.20.0
- Vercel runtime with Fluid compute
- Next.js: 16 (App Router)
@clerk/nextjs: 6.x
Root cause
Effect's MixedScheduler batches fiber continuations and drains them inside a single microtask or timer callback. The AsyncLocalStorage context active during that callback belongs to whichever request first triggered the scheduler's drain cycle — not the request that owns the fiber being resumed.
Detailed mechanism
1. Scheduler batching (effect/src/Scheduler.ts, MixedScheduler)
```typescript
// MixedScheduler.starve() — called once when first task is scheduled
private starve(depth = 0) {
if (depth >= this.maxNextTickBeforeTimer) {
setTimeout(() => this.starveInternal(0), 0) // timer queue
} else {
Promise.resolve(void 0).then(() => this.starveInternal(depth + 1)) // microtask queue
}
}
// MixedScheduler.starveInternal() — drains ALL accumulated tasks in one call
private starveInternal(depth: number) {
const tasks = this.tasks.buckets
this.tasks.buckets = []
for (const [_, toRun] of tasks) {
for (let i = 0; i < toRun.length; i++) {
toRun[i]() // ← Every fiber continuation runs in the SAME ALS context
}
}
// ...
}
```
scheduleTask only calls starve() when running is false. Subsequent tasks accumulate in this.tasks until starveInternal drains them all. The Promise.then() (or setTimeout) callback inherits the ALS context from whichever call site created it — i.e., whichever request's fiber first set running = true.
Result: Under concurrent load, fiber continuations from Request A and Request B execute inside the same starveInternal call, sharing a single ALS context. If Request A triggered starve(), then Request B's fiber reads Request A's ALS context.
2. toWebHandlerRuntime does not propagate ALS (@effect/platform/src/HttpApp.ts:211-240)
```typescript
export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
const httpRuntime: Types.Mutable<Runtime.Runtime<R>> = Runtime.make(runtime)
const run = Runtime.runFork(httpRuntime)
return <E>(self: Default<E, R | Scope.Scope>, middleware?) => {
return (request: Request, context?): Promise<Response> =>
new Promise((resolve) => {
// Per-request Effect context is correctly set via contextMap:
const contextMap = new Map<string, any>(runtime.context.unsafeMap)
const httpServerRequest = ServerRequest.fromWeb(request)
contextMap.set(ServerRequest.HttpServerRequest.key, httpServerRequest)
httpRuntime.context = Context.unsafeMake(contextMap)
// But the fiber is forked without any ALS propagation:
const fiber = run(httpApp as any) // ← ALS context is NOT captured or restored
})
}
}
```
Effect's own Context (containing HttpServerRequest) is correctly set per-request. But the Node.js ALS context — which frameworks like Next.js, Clerk, and OpenTelemetry rely on — is not captured at fork time or restored when the fiber's continuations execute.
3. The dangerous pattern this enables
```typescript
// RPC handler — runs inside an Effect fiber
const handler = Effect.gen(function*() {
// This calls auth() from @clerk/nextjs/server, which reads from ALS
const { userId } = yield* Effect.tryPromise({
try: async () => auth(), // ← may read WRONG user's session
catch: () => new UnauthorizedError({ message: "Auth failed" })
})
return yield* repository.getUser(userId)
})
```
The async () => auth() thunk executes when the fiber continuation is scheduled by MixedScheduler. At that point, the ALS context belongs to an arbitrary concurrent request.
Reproduction scenario
```
Timeline (two concurrent requests to the same toWebHandler endpoint):
T0: Request A arrives → POST handler → webHandler(requestA)
→ Promise executor runs synchronously
→ httpRuntime.context set to A's context
→ fiber A forked, runs first ops synchronously
→ fiber A yields (e.g., at Effect.tryPromise boundary)
→ scheduler.scheduleTask(fiberA_continuation)
→ running=false → starve() called → Promise.resolve().then(drain)
↑ ALS context captured = Request A's context
T1: Request B arrives → POST handler → webHandler(requestB)
→ Promise executor runs synchronously
→ httpRuntime.context set to B's context
→ fiber B forked, runs first ops synchronously
→ fiber B yields
→ scheduler.scheduleTask(fiberB_continuation)
→ running=true → task queued, no new starve()
T2: Microtask fires → starveInternal() runs
→ Drains fiberA_continuation → auth() reads ALS → gets A's context ✓
→ Drains fiberB_continuation → auth() reads ALS → gets A's context ✗ ← WRONG USER
```
Minimal reproduction
```typescript
import { AsyncLocalSto
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is high, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality high, integrity high, availability none.
Weakness class
CVE-2026-32887 is classified as CWE-362: Race Condition. Concurrent operations share state without proper synchronisation, so timing decides whether the result is correct.
Affected software
CVE-2026-32887 is recorded against 1 package.
- effect
Timeline and source
Published on 20 March 2026 and last revised on 17 June 2026. A public exploit is known to exist, which raises the urgency of patching considerably. Record sourced from OSV.
References
Details
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| effect | — | — |
References
Similar Threats
- Unknown MAL-2026-5245
- Unknown MAL-2025-101890
- Unknown MAL-2025-101891
- Unknown MAL-2025-101892
- Unknown MAL-2025-67949
More CVE 2026 advisories
Browse all of CVE 2026 in the advisory index.
Exploit Protection
Are you running effect?
CVE-2026-32887 carries CVSS 8.0 High rating and a public exploit already exists. BotEraser checks your installation against this and other known CVE records, and blocks IPs associated with exploit activity.
Check My Site For CVE-2026-32887 →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.