VYPR
Medium severity6.0NVD Advisory· Published May 11, 2026· Updated May 13, 2026

CVE-2026-45005

CVE-2026-45005

Description

OpenClaw before 2026.4.23 caches resolved webhook route secrets backed by SecretRef values, allowing stale secrets to remain valid after rotation and reload. Attackers with previously valid webhook route secrets can continue authenticating requests and invoking configured webhook task flows until gateway or plugin restart.

Affected products

1

Patches

1
36c4a372a0ad

fix(webhooks): reload route secrets per request (#70727)

https://github.com/openclaw/openclawDevin RobisonApr 23, 2026via nvd-ref
3 files changed · +27 13
  • CHANGELOG.md+1 0 modified
    @@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
     - Providers/Anthropic Vertex: restore ADC-backed model discovery after the lightweight provider-discovery path by resolving emitted discovery entries, exposing synthetic auth on bootstrap discovery, and honoring copied env snapshots when probing the default GCP ADC path. Fixes #65715. (#65716) Thanks @feiskyer.
     - Codex harness/status: pin embedded harness selection per session, show active non-PI harness ids such as `codex` in `/status`, and keep legacy transcripts on PI until `/new` or `/reset` so config changes cannot hot-switch existing sessions.
     - Gateway/security: fail closed on agent-driven `gateway config.apply`/`config.patch` runtime edits by allowlisting a narrow set of agent-tunable prompt, model, and mention-gating paths (including Telegram topic-level `requireMention`) instead of relying on a hand-maintained denylist of protected subtrees that could miss new sensitive config keys. (#70726) Thanks @drobison00.
    +- Webhooks/security: re-resolve `SecretRef`-backed webhook route secrets on each request so `openclaw secrets reload` revokes the previous secret immediately instead of waiting for a gateway restart. (#70727) Thanks @drobison00.
     
     ## 2026.4.22
     
    
  • extensions/webhooks/src/http.test.ts+18 4 modified
    @@ -158,9 +158,10 @@ describe("createTaskFlowWebhookRequestHandler", () => {
         expect(res.statusCode).toBe(401);
         expect(res.body).toBe("unauthorized");
         expect(target.taskFlow.list()).toEqual([]);
    +    expect(hoisted.resolveConfiguredSecretInputStringMock).not.toHaveBeenCalled();
       });
     
    -  it("caches SecretRef resolution across requests for the same route", async () => {
    +  it("re-resolves SecretRef-backed secrets across requests", async () => {
         const runtime = createRuntimeTaskFlow();
         const target: TaskFlowWebhookTarget = {
           routeId: "cached",
    @@ -176,7 +177,10 @@ describe("createTaskFlowWebhookRequestHandler", () => {
             sessionKey: "agent:main:webhook-cached",
           }),
         };
    -    hoisted.resolveConfiguredSecretInputStringMock.mockResolvedValue({ value: "shared-secret" });
    +    hoisted.resolveConfiguredSecretInputStringMock
    +      .mockResolvedValueOnce({ value: "shared-secret" })
    +      .mockResolvedValueOnce({ value: "rotated-secret" })
    +      .mockResolvedValueOnce({ value: "rotated-secret" });
         const handler = createHandlerWithTarget(target);
     
         const first = await dispatchJsonRequest({
    @@ -195,10 +199,20 @@ describe("createTaskFlowWebhookRequestHandler", () => {
             action: "list_flows",
           },
         });
    +    const third = await dispatchJsonRequest({
    +      handler,
    +      path: target.path,
    +      secret: "rotated-secret",
    +      body: {
    +        action: "list_flows",
    +      },
    +    });
     
         expect(first.statusCode).toBe(200);
    -    expect(second.statusCode).toBe(200);
    -    expect(hoisted.resolveConfiguredSecretInputStringMock).toHaveBeenCalledTimes(1);
    +    expect(second.statusCode).toBe(401);
    +    expect(second.body).toBe("unauthorized");
    +    expect(third.statusCode).toBe(200);
    +    expect(hoisted.resolveConfiguredSecretInputStringMock).toHaveBeenCalledTimes(3);
       });
     
       it("creates flows through the bound session and scrubs owner metadata from responses", async () => {
    
  • extensions/webhooks/src/http.ts+8 9 modified
    @@ -667,7 +667,6 @@ export function createTaskFlowWebhookRequestHandler(params: {
       targetsByPath: Map<string, TaskFlowWebhookTarget[]>;
       inFlightLimiter?: WebhookInFlightLimiter;
     }): (req: IncomingMessage, res: ServerResponse) => Promise<boolean> {
    -  const secretByTarget = new WeakMap<TaskFlowWebhookTarget, Promise<string | undefined>>();
       const rateLimiter = createFixedWindowRateLimiter({
         windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
         maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
    @@ -679,19 +678,19 @@ export function createTaskFlowWebhookRequestHandler(params: {
           maxInFlightPerKey: WEBHOOK_IN_FLIGHT_DEFAULTS.maxInFlightPerKey,
           maxTrackedKeys: WEBHOOK_IN_FLIGHT_DEFAULTS.maxTrackedKeys,
         });
    -  const resolveTargetSecret = (target: TaskFlowWebhookTarget): Promise<string | undefined> => {
    -    const cached = secretByTarget.get(target);
    -    if (cached) {
    -      return cached;
    +  const resolveTargetSecret = async (
    +    target: TaskFlowWebhookTarget,
    +  ): Promise<string | undefined> => {
    +    if (typeof target.secretInput === "string") {
    +      return target.secretInput;
         }
    -    const pending = resolveConfiguredSecretInputString({
    +    const resolved = await resolveConfiguredSecretInputString({
           config: params.cfg,
           env: process.env,
           value: target.secretInput,
           path: target.secretConfigPath,
    -    }).then((resolved) => resolved.value);
    -    secretByTarget.set(target, pending);
    -    return pending;
    +    });
    +    return resolved.value;
       };
     
       return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

3

News mentions

21