diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adc98933..f6d700dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -299,6 +299,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/OpenResponses: harden URL-based `input_file`/`input_image` handling with explicit SSRF deny policy, hostname allowlists (`files.urlAllowlist` / `images.urlAllowlist`), per-request URL input caps (`maxUrlParts`), blocked-fetch audit logging, and regression coverage/docs updates. +- Sessions: guard `withSessionStoreLock` against undefined `storePath` to prevent `path.dirname` crash. (#14717) - Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek. - Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc. - Security/Audit: add hook session-routing hardening checks (`hooks.defaultSessionKey`, `hooks.allowRequestSessionKey`, and prefix allowlists), and warn when HTTP API endpoints allow explicit session-key routing. diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 741763b04..482b33590 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -714,6 +714,11 @@ async function withSessionStoreLock( fn: () => Promise, opts: SessionStoreLockOptions = {}, ): Promise { + if (!storePath || typeof storePath !== "string") { + throw new Error( + `withSessionStoreLock: storePath must be a non-empty string, got ${JSON.stringify(storePath)}`, + ); + } const timeoutMs = opts.timeoutMs ?? 10_000; const staleMs = opts.staleMs ?? 30_000; // `pollIntervalMs` is retained for API compatibility with older lock options. diff --git a/src/config/sessions/store.undefined-path.test.ts b/src/config/sessions/store.undefined-path.test.ts new file mode 100644 index 000000000..8d0bc1b05 --- /dev/null +++ b/src/config/sessions/store.undefined-path.test.ts @@ -0,0 +1,23 @@ +/** + * Regression test for #14717: path.dirname(undefined) crash in withSessionStoreLock + * + * When a channel plugin passes undefined as storePath to recordSessionMetaFromInbound, + * the call chain reaches withSessionStoreLock → path.dirname(undefined) → TypeError crash. + * After fix, a clear Error is thrown instead of an unhandled TypeError. + */ +import { describe, expect, it } from "vitest"; +import { updateSessionStore } from "./store.js"; + +describe("withSessionStoreLock storePath guard (#14717)", () => { + it("throws descriptive error when storePath is undefined", async () => { + await expect( + updateSessionStore(undefined as unknown as string, (store) => store), + ).rejects.toThrow("withSessionStoreLock: storePath must be a non-empty string"); + }); + + it("throws descriptive error when storePath is empty string", async () => { + await expect(updateSessionStore("", (store) => store)).rejects.toThrow( + "withSessionStoreLock: storePath must be a non-empty string", + ); + }); +});