GHSA-38F7-945M-QR2G

Vulnerability from github – Published: 2026-03-20 20:34 – Updated: 2026-03-25 18:11
VLAI?
Summary
Effect `AsyncLocalStorage` context lost/contaminated inside Effect fibers under concurrent load with RPC
Details

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

import { AsyncLocalStorage } from "node:async_hooks"
import { Effect, Layer } from "effect"
import { RpcServer, RpcSerialization, Rpc, RpcGroup } from "@effect/rpc"
import { HttpServer } from "@effect/platform"
import * as S from "effect/Schema"

// Simulate a framework's ALS (like Next.js / Clerk)
const requestStore = new AsyncLocalStorage<{ userId: string }>()

class GetUser extends Rpc.make("GetUser", {
  success: S.Struct({ userId: S.String, alsUserId: S.String }),
  failure: S.Never,
  payload: {}
}) {}

const MyRpc = RpcGroup.make("MyRpc").add(GetUser)

const MyRpcLive = MyRpc.toLayer(
  RpcGroup.toHandlers(MyRpc, {
    GetUser: () =>
      Effect.gen(function*() {
        // Simulate calling an ALS-dependent API inside an Effect fiber
        const alsResult = yield* Effect.tryPromise({
          try: async () => {
            const store = requestStore.getStore()
            return store?.userId ?? "NONE"
          },
          catch: () => { throw new Error("impossible") }
        })
        return { userId: "from-effect-context", alsUserId: alsResult }
      })
  })
)

const RpcLayer = MyRpcLive.pipe(
  Layer.provideMerge(RpcSerialization.layerJson),
  Layer.provideMerge(HttpServer.layerContext)
)

const { handler } = RpcServer.toWebHandler(MyRpc, { layer: RpcLayer })

// Simulate two concurrent requests with different ALS contexts
async function main() {
  const results = await Promise.all([
    requestStore.run({ userId: "user-A" }, () => handler(makeRpcRequest("GetUser"))),
    requestStore.run({ userId: "user-B" }, () => handler(makeRpcRequest("GetUser"))),
  ])

  // Parse responses and check if alsUserId matches the expected user
  // Under the bug: both responses may show "user-A" (or one shows the other's)
  for (const res of results) {
    console.log(await res.json())
  }
}

Impact

Symptom Severity
auth() returns wrong user's session Critical — authentication bypass
cookies() / headers() from Next.js read wrong request High — data leakage
OpenTelemetry trace context crosses requests Medium — incorrect traces
Works locally, fails in production Hard to diagnose — only manifests under concurrent load

Workaround

Capture ALS-dependent values before entering the Effect runtime and pass them via Effect's own context system:

// In the route handler — OUTSIDE the Effect fiber (ALS is correct here)
export const POST = async (request: Request) => {
  const { userId } = await auth()  // ← Safe: still in Next.js ALS context

  // Inject into request headers or use the `context` parameter
  const headers = new Headers(request.headers)
  headers.set("x-clerk-auth-user-id", userId ?? "")
  const enrichedRequest = new Request(request.url, {
    method: request.method,
    headers,
    body: request.body,
    duplex: "half" as any,
  })

  return webHandler(enrichedRequest)
}

// In Effect handlers — read from HttpServerRequest headers instead of calling auth()
const getAuthenticatedUserId = Effect.gen(function*() {
  const req = yield* HttpServerRequest.HttpServerRequest
  const userId = req.headers["x-clerk-auth-user-id"]
  if (!userId) return yield* Effect.fail(new UnauthorizedError({ message: "Auth required" }))
  return userId
})

Suggested fix (for Effect maintainers)

Option A: Propagate ALS context through the scheduler

Capture the AsyncLocalStorage snapshot when a fiber continuation is scheduled, and restore it when the continuation executes:

// In MixedScheduler or the fiber runtime
import { AsyncLocalStorage } from "node:async_hooks"

scheduleTask(task: Task, priority: number) {
  // Capture current ALS context
  const snapshot = AsyncLocalStorage.snapshot()
  this.tasks.scheduleTask(() => snapshot(task), priority)
  // ...
}

AsyncLocalStorage.snapshot() (Node.js 20.5+) returns a function that, when called, restores the ALS context from the point of capture. This ensures each fiber continuation runs with its originating request's ALS context.

Trade-off: Adds one closure allocation per scheduled task. Could be opt-in via a FiberRef or scheduler option.

Option B: Capture ALS at runFork and restore per fiber step

When Runtime.runFork is called, capture the ALS snapshot and associate it with the fiber. Before each fiber step (in the fiber runtime's evaluateEffect loop), restore the snapshot.

Trade-off: More invasive but provides correct ALS propagation for the fiber's entire lifetime, including across flatMap chains and Effect.tryPromise thunks.

Option C: Document the limitation and provide a context injection API

If ALS propagation is intentionally not supported, document this prominently and provide a first-class API for toWebHandler to accept per-request context. The existing context?: Context.Context<never> parameter on the handler function partially addresses this, but it requires callers to know about the issue and manually extract values before entering Effect.

Related

  • Node.js AsyncLocalStorage docs: https://nodejs.org/api/async_context.html
  • AsyncLocalStorage.snapshot(): https://nodejs.org/api/async_context.html#static-method-asynclocalstoragesnapshot
  • Next.js uses ALS for cookies(), headers(), auth() in App Router
  • Similar issue pattern in other fiber-based runtimes (e.g., ZIO has FiberRef propagation for this)

POC replica of my setup

// Create web handler from Effect RPC
// sharedMemoMap ensures all RPC routes share the same connection pool
const { handler: webHandler, dispose } = RpcServer.toWebHandler(DemoRpc, {
  layer: RpcLayer,
  memoMap: sharedMemoMap,
});

/**
 * POST /api/rpc/demo
 */
export const POST = async (request: Request) => {
  return webHandler(request);
};

registerDispose(dispose);

Used util functions


/**
 * Creates a dispose registry that collects dispose callbacks and runs them
 * when `runAll` is invoked. Handles both sync and async dispose functions,
 * catching errors to prevent one failing dispose from breaking others.
 *
 * @internal Exported for testing — use `registerDispose` in application code.
 */
export const makeDisposeRegistry = () => {
  const disposeFns: Array<() => void | Promise<void>> = []

  const runAll = () => {
    for (const fn of disposeFns) {
      try {
        const result = fn()
        if (result && typeof result.then === "function") {
          result.then(undefined, (err: unknown) => console.error("Dispose error:", err))
        }
      } catch (err) {
        console.error("Dispose error:", err)
      }
    }
  }

  const register = (dispose: () => void | Promise<void>) => {
    disposeFns.push(dispose)
  }

  return { register, runAll }
}

export const registerDispose: (dispose: () => void | Promise<void>) => void = globalValue(
  Symbol.for("@global/RegisterDispose"),
  () => {
    const registry = makeDisposeRegistry()

    if (typeof process !== "undefined") {
      process.once("beforeExit", registry.runAll)
    }

    return registry.register
  }
)

The actual effect that was run within the RPC context that the bug was found

``` export const getAuthenticatedUserId: Effect.Effect = Effect.gen(function() { const authResult = yield Effect.tryPromise({ try: async () => auth(), catch: () => new UnauthorizedError({ message: "Failed to get auth session" }) })

if (!authResult.userId) {
  return yield* Effect.fail(
    new UnauthorizedError({
      message: "Authentication required"
    })
  )
}

return authResult.userId

}) ```

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "effect"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.20.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-32887"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-362"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-20T20:34:06Z",
    "nvd_published_at": "2026-03-20T22:16:27Z",
    "severity": "HIGH"
  },
  "details": "## Versions\n\n- `effect`: 3.19.15\n- `@effect/rpc`: 0.72.1\n- `@effect/platform`: 0.94.2\n- Node.js: v22.20.0\n- Vercel runtime with Fluid compute\n- Next.js: 16 (App Router)\n- `@clerk/nextjs`: 6.x\n\n## Root cause\n\nEffect\u0027s `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\u0027s drain cycle \u2014 **not** the request that owns the fiber being resumed.\n\n### Detailed mechanism\n\n#### 1. Scheduler batching (`effect/src/Scheduler.ts`, `MixedScheduler`)\n\n```typescript\n// MixedScheduler.starve() \u2014 called once when first task is scheduled\nprivate starve(depth = 0) {\n  if (depth \u003e= this.maxNextTickBeforeTimer) {\n    setTimeout(() =\u003e this.starveInternal(0), 0)       // timer queue\n  } else {\n    Promise.resolve(void 0).then(() =\u003e this.starveInternal(depth + 1)) // microtask queue\n  }\n}\n\n// MixedScheduler.starveInternal() \u2014 drains ALL accumulated tasks in one call\nprivate starveInternal(depth: number) {\n  const tasks = this.tasks.buckets\n  this.tasks.buckets = []\n  for (const [_, toRun] of tasks) {\n    for (let i = 0; i \u003c toRun.length; i++) {\n      toRun[i]()  // \u2190 Every fiber continuation runs in the SAME ALS context\n    }\n  }\n  // ...\n}\n```\n\n`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 \u2014 i.e., whichever request\u0027s fiber first set `running = true`.\n\n**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\u0027s fiber reads Request A\u0027s ALS context.\n\n#### 2. `toWebHandlerRuntime` does not propagate ALS (`@effect/platform/src/HttpApp.ts:211-240`)\n\n```typescript\nexport const toWebHandlerRuntime = \u003cR\u003e(runtime: Runtime.Runtime\u003cR\u003e) =\u003e {\n  const httpRuntime: Types.Mutable\u003cRuntime.Runtime\u003cR\u003e\u003e = Runtime.make(runtime)\n  const run = Runtime.runFork(httpRuntime)\n  return \u003cE\u003e(self: Default\u003cE, R | Scope.Scope\u003e, middleware?) =\u003e {\n    return (request: Request, context?): Promise\u003cResponse\u003e =\u003e\n      new Promise((resolve) =\u003e {\n        // Per-request Effect context is correctly set via contextMap:\n        const contextMap = new Map\u003cstring, any\u003e(runtime.context.unsafeMap)\n        const httpServerRequest = ServerRequest.fromWeb(request)\n        contextMap.set(ServerRequest.HttpServerRequest.key, httpServerRequest)\n        httpRuntime.context = Context.unsafeMake(contextMap)\n\n        // But the fiber is forked without any ALS propagation:\n        const fiber = run(httpApp as any)  // \u2190 ALS context is NOT captured or restored\n      })\n  }\n}\n```\n\nEffect\u0027s own `Context` (containing `HttpServerRequest`) is correctly set per-request. But the **Node.js ALS context** \u2014 which frameworks like Next.js, Clerk, and OpenTelemetry rely on \u2014 is not captured at fork time or restored when the fiber\u0027s continuations execute.\n\n#### 3. The dangerous pattern this enables\n\n```typescript\n// RPC handler \u2014 runs inside an Effect fiber\nconst handler = Effect.gen(function*() {\n  // This calls auth() from @clerk/nextjs/server, which reads from ALS\n  const { userId } = yield* Effect.tryPromise({\n    try: async () =\u003e auth(),  // \u2190 may read WRONG user\u0027s session\n    catch: () =\u003e new UnauthorizedError({ message: \"Auth failed\" })\n  })\n  return yield* repository.getUser(userId)\n})\n```\n\nThe `async () =\u003e auth()` thunk executes when the fiber continuation is scheduled by `MixedScheduler`. At that point, the ALS context belongs to an arbitrary concurrent request.\n\n## Reproduction scenario\n\n```\nTimeline (two concurrent requests to the same toWebHandler endpoint):\n\nT0: Request A arrives \u2192 POST handler \u2192 webHandler(requestA)\n    \u2192 Promise executor runs synchronously\n    \u2192 httpRuntime.context set to A\u0027s context\n    \u2192 fiber A forked, runs first ops synchronously\n    \u2192 fiber A yields (e.g., at Effect.tryPromise boundary)\n    \u2192 scheduler.scheduleTask(fiberA_continuation)\n    \u2192 running=false \u2192 starve() called \u2192 Promise.resolve().then(drain)\n       \u2191 ALS context captured = Request A\u0027s context\n\nT1: Request B arrives \u2192 POST handler \u2192 webHandler(requestB)\n    \u2192 Promise executor runs synchronously\n    \u2192 httpRuntime.context set to B\u0027s context\n    \u2192 fiber B forked, runs first ops synchronously\n    \u2192 fiber B yields\n    \u2192 scheduler.scheduleTask(fiberB_continuation)\n    \u2192 running=true \u2192 task queued, no new starve()\n\nT2: Microtask fires \u2192 starveInternal() runs\n    \u2192 Drains fiberA_continuation \u2192 auth() reads ALS \u2192 gets A\u0027s context \u2713\n    \u2192 Drains fiberB_continuation \u2192 auth() reads ALS \u2192 gets A\u0027s context \u2717 \u2190 WRONG USER\n```\n\n## Minimal reproduction\n\n```typescript\nimport { AsyncLocalStorage } from \"node:async_hooks\"\nimport { Effect, Layer } from \"effect\"\nimport { RpcServer, RpcSerialization, Rpc, RpcGroup } from \"@effect/rpc\"\nimport { HttpServer } from \"@effect/platform\"\nimport * as S from \"effect/Schema\"\n\n// Simulate a framework\u0027s ALS (like Next.js / Clerk)\nconst requestStore = new AsyncLocalStorage\u003c{ userId: string }\u003e()\n\nclass GetUser extends Rpc.make(\"GetUser\", {\n  success: S.Struct({ userId: S.String, alsUserId: S.String }),\n  failure: S.Never,\n  payload: {}\n}) {}\n\nconst MyRpc = RpcGroup.make(\"MyRpc\").add(GetUser)\n\nconst MyRpcLive = MyRpc.toLayer(\n  RpcGroup.toHandlers(MyRpc, {\n    GetUser: () =\u003e\n      Effect.gen(function*() {\n        // Simulate calling an ALS-dependent API inside an Effect fiber\n        const alsResult = yield* Effect.tryPromise({\n          try: async () =\u003e {\n            const store = requestStore.getStore()\n            return store?.userId ?? \"NONE\"\n          },\n          catch: () =\u003e { throw new Error(\"impossible\") }\n        })\n        return { userId: \"from-effect-context\", alsUserId: alsResult }\n      })\n  })\n)\n\nconst RpcLayer = MyRpcLive.pipe(\n  Layer.provideMerge(RpcSerialization.layerJson),\n  Layer.provideMerge(HttpServer.layerContext)\n)\n\nconst { handler } = RpcServer.toWebHandler(MyRpc, { layer: RpcLayer })\n\n// Simulate two concurrent requests with different ALS contexts\nasync function main() {\n  const results = await Promise.all([\n    requestStore.run({ userId: \"user-A\" }, () =\u003e handler(makeRpcRequest(\"GetUser\"))),\n    requestStore.run({ userId: \"user-B\" }, () =\u003e handler(makeRpcRequest(\"GetUser\"))),\n  ])\n\n  // Parse responses and check if alsUserId matches the expected user\n  // Under the bug: both responses may show \"user-A\" (or one shows the other\u0027s)\n  for (const res of results) {\n    console.log(await res.json())\n  }\n}\n```\n\n## Impact\n\n| Symptom | Severity |\n|---------|----------|\n| `auth()` returns wrong user\u0027s session | **Critical** \u2014 authentication bypass |\n| `cookies()` / `headers()` from Next.js read wrong request | **High** \u2014 data leakage |\n| OpenTelemetry trace context crosses requests | **Medium** \u2014 incorrect traces |\n| Works locally, fails in production | Hard to diagnose \u2014 only manifests under concurrent load |\n\n## Workaround\n\nCapture ALS-dependent values **before** entering the Effect runtime and pass them via Effect\u0027s own context system:\n\n```typescript\n// In the route handler \u2014 OUTSIDE the Effect fiber (ALS is correct here)\nexport const POST = async (request: Request) =\u003e {\n  const { userId } = await auth()  // \u2190 Safe: still in Next.js ALS context\n\n  // Inject into request headers or use the `context` parameter\n  const headers = new Headers(request.headers)\n  headers.set(\"x-clerk-auth-user-id\", userId ?? \"\")\n  const enrichedRequest = new Request(request.url, {\n    method: request.method,\n    headers,\n    body: request.body,\n    duplex: \"half\" as any,\n  })\n\n  return webHandler(enrichedRequest)\n}\n\n// In Effect handlers \u2014 read from HttpServerRequest headers instead of calling auth()\nconst getAuthenticatedUserId = Effect.gen(function*() {\n  const req = yield* HttpServerRequest.HttpServerRequest\n  const userId = req.headers[\"x-clerk-auth-user-id\"]\n  if (!userId) return yield* Effect.fail(new UnauthorizedError({ message: \"Auth required\" }))\n  return userId\n})\n```\n\n## Suggested fix (for Effect maintainers)\n\n### Option A: Propagate ALS context through the scheduler\n\nCapture the `AsyncLocalStorage` snapshot when a fiber continuation is scheduled, and restore it when the continuation executes:\n\n```typescript\n// In MixedScheduler or the fiber runtime\nimport { AsyncLocalStorage } from \"node:async_hooks\"\n\nscheduleTask(task: Task, priority: number) {\n  // Capture current ALS context\n  const snapshot = AsyncLocalStorage.snapshot()\n  this.tasks.scheduleTask(() =\u003e snapshot(task), priority)\n  // ...\n}\n```\n\n`AsyncLocalStorage.snapshot()` (Node.js 20.5+) returns a function that, when called, restores the ALS context from the point of capture. This ensures each fiber continuation runs with its originating request\u0027s ALS context.\n\n**Trade-off:** Adds one closure allocation per scheduled task. Could be opt-in via a `FiberRef` or scheduler option.\n\n### Option B: Capture ALS at `runFork` and restore per fiber step\n\nWhen `Runtime.runFork` is called, capture the ALS snapshot and associate it with the fiber. Before each fiber step (in the fiber runtime\u0027s `evaluateEffect` loop), restore the snapshot.\n\n**Trade-off:** More invasive but provides correct ALS propagation for the fiber\u0027s entire lifetime, including across `flatMap` chains and `Effect.tryPromise` thunks.\n\n### Option C: Document the limitation and provide a `context` injection API\n\nIf ALS propagation is intentionally not supported, document this prominently and provide a first-class API for `toWebHandler` to accept per-request context. The existing `context?: Context.Context\u003cnever\u003e` parameter on the handler function partially addresses this, but it requires callers to know about the issue and manually extract values before entering Effect.\n\n## Related\n\n- Node.js `AsyncLocalStorage` docs: https://nodejs.org/api/async_context.html\n- `AsyncLocalStorage.snapshot()`: https://nodejs.org/api/async_context.html#static-method-asynclocalstoragesnapshot\n- Next.js uses ALS for `cookies()`, `headers()`, `auth()` in App Router\n- Similar issue pattern in other fiber-based runtimes (e.g., ZIO has `FiberRef` propagation for this)\n\n\n## POC replica of my setup\n\n```\n// Create web handler from Effect RPC\n// sharedMemoMap ensures all RPC routes share the same connection pool\nconst { handler: webHandler, dispose } = RpcServer.toWebHandler(DemoRpc, {\n  layer: RpcLayer,\n  memoMap: sharedMemoMap,\n});\n\n/**\n * POST /api/rpc/demo\n */\nexport const POST = async (request: Request) =\u003e {\n  return webHandler(request);\n};\n\nregisterDispose(dispose);\n```\n\n### Used util functions\n\n```\n\n/**\n * Creates a dispose registry that collects dispose callbacks and runs them\n * when `runAll` is invoked. Handles both sync and async dispose functions,\n * catching errors to prevent one failing dispose from breaking others.\n *\n * @internal Exported for testing \u2014 use `registerDispose` in application code.\n */\nexport const makeDisposeRegistry = () =\u003e {\n  const disposeFns: Array\u003c() =\u003e void | Promise\u003cvoid\u003e\u003e = []\n\n  const runAll = () =\u003e {\n    for (const fn of disposeFns) {\n      try {\n        const result = fn()\n        if (result \u0026\u0026 typeof result.then === \"function\") {\n          result.then(undefined, (err: unknown) =\u003e console.error(\"Dispose error:\", err))\n        }\n      } catch (err) {\n        console.error(\"Dispose error:\", err)\n      }\n    }\n  }\n\n  const register = (dispose: () =\u003e void | Promise\u003cvoid\u003e) =\u003e {\n    disposeFns.push(dispose)\n  }\n\n  return { register, runAll }\n}\n\nexport const registerDispose: (dispose: () =\u003e void | Promise\u003cvoid\u003e) =\u003e void = globalValue(\n  Symbol.for(\"@global/RegisterDispose\"),\n  () =\u003e {\n    const registry = makeDisposeRegistry()\n\n    if (typeof process !== \"undefined\") {\n      process.once(\"beforeExit\", registry.runAll)\n    }\n\n    return registry.register\n  }\n)\n```\n\n### The actual effect that was run within the RPC context that the bug was found\n\n```\nexport const getAuthenticatedUserId: Effect.Effect\u003cstring, UnauthorizedError\u003e =\n  Effect.gen(function*() {\n    const authResult = yield* Effect.tryPromise({\n      try: async () =\u003e auth(),\n      catch: () =\u003e\n        new UnauthorizedError({\n          message: \"Failed to get auth session\"\n        })\n    })\n\n    if (!authResult.userId) {\n      return yield* Effect.fail(\n        new UnauthorizedError({\n          message: \"Authentication required\"\n        })\n      )\n    }\n\n    return authResult.userId\n  })\n ```",
  "id": "GHSA-38f7-945m-qr2g",
  "modified": "2026-03-25T18:11:18Z",
  "published": "2026-03-20T20:34:06Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Effect-TS/effect/security/advisories/GHSA-38f7-945m-qr2g"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32887"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Effect-TS/effect"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Effect `AsyncLocalStorage` context lost/contaminated inside Effect fibers under concurrent load with RPC"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…