@mulmobridge/chat-service 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-state.d.ts +9 -0
- package/dist/chat-state.js +29 -0
- package/dist/index.js +12 -1
- package/package.json +1 -1
package/dist/chat-state.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export interface ChatStateStore {
|
|
|
22
22
|
connectSession(transportId: string, externalChatId: string, chatSessionId: string, roleId?: string): Promise<TransportChatState | null>;
|
|
23
23
|
generateSessionId(transportId: string, externalChatId: string): string;
|
|
24
24
|
}
|
|
25
|
+
/** True iff `sessionId` is safe to persist into transport state and later
|
|
26
|
+
* hand to session-metadata / event-log readers on the host side. Adds an
|
|
27
|
+
* explicit `..` rejection on top of `isSafeId` — the safe-id character
|
|
28
|
+
* class alone would accept the literal `..` and let a state file written
|
|
29
|
+
* by `/connect` poison downstream commands (e.g. `/history` reading the
|
|
30
|
+
* poisoned sessionId back through `readSessionJsonl`). Applied at the
|
|
31
|
+
* `/connect` route entry AND inside `connectSession` as defense-in-depth
|
|
32
|
+
* (issue #1896 follow-up to #1888 / #1895). */
|
|
33
|
+
export declare function isSafeSessionId(sessionId: string): boolean;
|
|
25
34
|
export declare function createChatStateStore(opts: {
|
|
26
35
|
transportsDir: string;
|
|
27
36
|
logger: Logger;
|
package/dist/chat-state.js
CHANGED
|
@@ -15,6 +15,23 @@ import { writeFileAtomic } from "./atomic-write.js";
|
|
|
15
15
|
function isSafeId(id) {
|
|
16
16
|
return /^[\w.-]+$/.test(id) && id.length > 0 && id.length <= 200;
|
|
17
17
|
}
|
|
18
|
+
/** True iff `sessionId` is safe to persist into transport state and later
|
|
19
|
+
* hand to session-metadata / event-log readers on the host side. Adds an
|
|
20
|
+
* explicit `..` rejection on top of `isSafeId` — the safe-id character
|
|
21
|
+
* class alone would accept the literal `..` and let a state file written
|
|
22
|
+
* by `/connect` poison downstream commands (e.g. `/history` reading the
|
|
23
|
+
* poisoned sessionId back through `readSessionJsonl`). Applied at the
|
|
24
|
+
* `/connect` route entry AND inside `connectSession` as defense-in-depth
|
|
25
|
+
* (issue #1896 follow-up to #1888 / #1895). */
|
|
26
|
+
export function isSafeSessionId(sessionId) {
|
|
27
|
+
if (typeof sessionId !== "string")
|
|
28
|
+
return false;
|
|
29
|
+
if (!isSafeId(sessionId))
|
|
30
|
+
return false;
|
|
31
|
+
if (sessionId.includes(".."))
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
18
35
|
// ── Factory ──────────────────────────────────────────────────
|
|
19
36
|
export function createChatStateStore(opts) {
|
|
20
37
|
const { transportsDir, logger } = opts;
|
|
@@ -58,6 +75,18 @@ export function createChatStateStore(opts) {
|
|
|
58
75
|
return state;
|
|
59
76
|
};
|
|
60
77
|
const connectSession = async (transportId, externalChatId, chatSessionId, roleId) => {
|
|
78
|
+
// Defense-in-depth: even though the /connect route validates chatSessionId
|
|
79
|
+
// at entry, refuse to persist an unsafe value here too. Otherwise a caller
|
|
80
|
+
// that bypasses the route (test harness, alternate transport, direct store
|
|
81
|
+
// access) could still write a hostile sessionId into the state file — and
|
|
82
|
+
// downstream commands like /history would later read that back into
|
|
83
|
+
// path-traversing filesystem operations. Return null so the route surfaces
|
|
84
|
+
// it as 404 (same as "no state for this chat"); either way the caller
|
|
85
|
+
// can't succeed with a hostile input. Issue #1896.
|
|
86
|
+
if (!isSafeSessionId(chatSessionId)) {
|
|
87
|
+
logger.warn("chat-state", "refused to connect unsafe sessionId", { transportId, externalChatId });
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
61
90
|
const existing = await getChatState(transportId, externalChatId);
|
|
62
91
|
if (!existing)
|
|
63
92
|
return null;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// standalone npm package without internal edits. See #269 / #305.
|
|
18
18
|
import { Router } from "express";
|
|
19
19
|
import { CHAT_SERVICE_ROUTES } from "@mulmobridge/protocol";
|
|
20
|
-
import { createChatStateStore } from "./chat-state.js";
|
|
20
|
+
import { createChatStateStore, isSafeSessionId } from "./chat-state.js";
|
|
21
21
|
import { createCommandHandler } from "./commands.js";
|
|
22
22
|
import { createRelay } from "./relay.js";
|
|
23
23
|
import { createPushQueue } from "./push-queue.js";
|
|
@@ -97,6 +97,17 @@ export function createChatService(deps) {
|
|
|
97
97
|
badRequest(res, "chatSessionId is required");
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
|
+
// Reject hostile / malformed sessionIds at the entry so they can't be
|
|
101
|
+
// persisted into transport state. Without this gate a caller could POST
|
|
102
|
+
// `{"chatSessionId": "../../etc/x"}`, the value would land in the state
|
|
103
|
+
// file, and a later `/history` command would read it back and hand it to
|
|
104
|
+
// `readSessionJsonl` — whose backing reader is documented as "internal
|
|
105
|
+
// fixed paths only, no `..` traversal guard". Also defended inside
|
|
106
|
+
// `connectSession` for defense-in-depth (issue #1896 follow-up to #1895).
|
|
107
|
+
if (!isSafeSessionId(chatSessionId)) {
|
|
108
|
+
badRequest(res, "chatSessionId has an unsafe format");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
100
111
|
// Resolve the target session's role BEFORE calling connectSession so the
|
|
101
112
|
// persisted state's `roleId` tracks the new session's role — otherwise the
|
|
102
113
|
// next relay's `startChat` would resume the new session under the previous
|
package/package.json
CHANGED