Every CVE Search/ Get API key

CVE-2026-32887

Effect Bug: `AsyncLocalStorage` context lost/contaminated inside Effect fibers under concurrent load with RPC

7.4 HighFix available

Effect is a TypeScript framework that consists of several packages that work together to help build TypeScript applications. Prior to version 3.20.0, when using RpcServer.toWebHandler (or HttpApp.toWebHandlerRuntime) inside a Next.js App Router route handler, any Node.js AsyncLocalStorage-dependent API called from within an Effect fiber can read another concurrent request's context — or no context at all. Under production traffic, auth() from @clerk/nextjs/server returns a different user's session. Version 3.20.0 contains a fix for the issue.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
None

Exploit likelihood

0.28%Up 0.3 points since Mar 21, 2026
90%50%10%1%

What each source reports

CVE List Score used 7.4

Effect Bug: `AsyncLocalStorage` context lost/contaminated inside Effect fibers under concurrent load with RPC

Effect is a TypeScript framework that consists of several packages that work together to help build TypeScript applications. Prior to version 3.20.0, when using RpcServer.toWebHandler (or HttpApp.toWebHandlerRuntime) inside a Next.js App Router route handler, any Node.js AsyncLocalStorage-dependent API called from within an Effect fiber can read another concurrent request's context — or no context at all. Under production traffic, auth() from @clerk/nextjs/server returns a different user's session. Version 3.20.0 contains a fix for the issue.

Record
CVE-2026-32887
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-362
Updated
2026-11-02
EUVDEUVD-2026-13818 7.4

Effect is a TypeScript framework that consists of several packages that work together to help build TypeScript applications. Prior to version 3.20.0, when using RpcServer.toWebHandler (or HttpApp.toWebHandlerRuntime) inside a Next.js App Router route handler, any Node.js AsyncLocalStorage-dependent API called from within an Effect fiber can read another concurrent request's context — or no context at all. Under production traffic, auth() from @clerk/nextjs/server returns a different user's session. Version 3.20.0 contains a fix for the issue.

Record
EUVD-2026-13818
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-38f7-945m-qr2g 7.4

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)

// 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)

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

// 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…
Record
GHSA-38f7-945m-qr2g
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-362
Updated
2026-11-02
NVD 7.4

Effect is a TypeScript framework that consists of several packages that work together to help build TypeScript applications. Prior to version 3.20.0, when using RpcServer.toWebHandler (or HttpApp.toWebHandlerRuntime) inside a Next.js App Router route handler, any Node.js AsyncLocalStorage-dependent API called from within an Effect fiber can read another concurrent request's context — or no context at all. Under production traffic, auth() from @clerk/nextjs/server returns a different user's session. Version 3.20.0 contains a fix for the issue.

Record
CVE-2026-32887
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-362
Updated
2026-11-02
OSV 7.4

Effect Bug: `AsyncLocalStorage` context lost/contaminated inside Effect fibers under concurrent load with RPC

Effect is a TypeScript framework that consists of several packages that work together to help build TypeScript applications. Prior to version 3.20.0, when using RpcServer.toWebHandler (or HttpApp.toWebHandlerRuntime) inside a Next.js App Router route handler, any Node.js AsyncLocalStorage-dependent API called from within an Effect fiber can read another concurrent request's context — or no context at all. Under production traffic, auth() from @clerk/nextjs/server returns a different user's session. Version 3.20.0 contains a fix for the issue.

Record
CVE-2026-32887
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-362
Updated
2026-11-02
OSVGHSA-38f7-945m-qr2g 7.4

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)

// 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)

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

// 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…
Record
GHSA-38f7-945m-qr2g
CVSS 3.1
7.4 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-362
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
effect
npm
before 3.20.0
3.20.0

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-32887 is a high vulnerability (CVSS 7.4) in Effect-ts Effect, published March 20, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.28%, the 20th percentile. Fixed in effect 3.20.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open