diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 700481efd3..a5a220c223 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -25,17 +25,17 @@ jobs: name: Install runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 # Node 22.x ships npm 10, whose peer resolver crashes on MemoryCore's # vitest@4 peer graph ("Cannot read properties of null (reading 'edgesOut')"). # Node 24 ships npm 11, which resolves it; engines allows >=22.16.0. - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "24" - name: Cache node_modules - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 id: cache-deps with: path: MemoryCore/node_modules @@ -51,14 +51,14 @@ jobs: needs: install runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "24" - name: Restore node_modules - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: MemoryCore/node_modules key: deps-${{ runner.os }}-${{ hashFiles('MemoryCore/package.json') }} @@ -70,7 +70,7 @@ jobs: run: npm pack - name: Upload .tgz artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: package-tarball path: "MemoryCore/*.tgz" @@ -81,7 +81,7 @@ jobs: name: Manifest runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Validate openclaw.plugin.json run: | @@ -124,7 +124,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download tarball - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: package-tarball path: MemoryCore @@ -155,7 +155,7 @@ jobs: name: Skill Queue Isolation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 # 需要完整历史以便 git diff base..HEAD diff --git a/MemoryCore/src/gateway/chat-memory-e2e-helpers.ts b/MemoryCore/src/gateway/chat-memory-e2e-helpers.ts index 2c21eb4b7c..0a397fc743 100644 --- a/MemoryCore/src/gateway/chat-memory-e2e-helpers.ts +++ b/MemoryCore/src/gateway/chat-memory-e2e-helpers.ts @@ -90,12 +90,13 @@ export async function post( // ── user / team / agent 装置 ── let adminKey: string | undefined; +let fixtureSeq = 0; async function seedAdminOnce(): Promise { if (adminKey) return adminKey; const r = await post<{ user_key?: string; default_user_key?: string }>( "/v3/internal/meta/user/init-admin", - { username: `admin-${Math.random().toString(36).slice(2, 8)}` }, + { username: `admin-${++fixtureSeq}` }, ); expect(r.body.code).toBe(0); adminKey = r.body.data!.user_key ?? r.body.data!.default_user_key!; @@ -117,7 +118,7 @@ export async function makeFixture(tag: string, messageCount = 3): Promise( "/v3/meta/user/create", - { username: `u-${tag}-${Math.random().toString(36).slice(2, 6)}` }, + { username: `u-${tag}-${++fixtureSeq}` }, { userKey: key }, ); expect(u.body.code).toBe(0); diff --git a/MemoryCore/src/metadata/utils/crypto.ts b/MemoryCore/src/metadata/utils/crypto.ts index 4c3664b979..731adf6802 100644 --- a/MemoryCore/src/metadata/utils/crypto.ts +++ b/MemoryCore/src/metadata/utils/crypto.ts @@ -9,7 +9,7 @@ * v3.1 起元数据 User 域不再使用 password 哈希;本模块仅保留 generateUserKey 与历史 hash 工具。 */ -import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto"; +import { randomBytes, randomInt, scryptSync, timingSafeEqual } from "node:crypto"; import { USER_KEY_PREFIX } from "./user-key.js"; /** @@ -26,10 +26,9 @@ const PASSWORD_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01 /** 生成随机密码(默认 12 位,字符集:大小写字母、数字、下划线)。 */ export function generatePassword(length = 12): string { - const bytes = randomBytes(length); let out = ""; for (let i = 0; i < length; i++) { - out += PASSWORD_CHARSET[bytes[i]! % PASSWORD_CHARSET.length]; + out += PASSWORD_CHARSET[randomInt(PASSWORD_CHARSET.length)]; } return out; } diff --git a/MemoryCore/src/metadata/utils/id-generator.ts b/MemoryCore/src/metadata/utils/id-generator.ts index cfa3da3305..bee0dee619 100644 --- a/MemoryCore/src/metadata/utils/id-generator.ts +++ b/MemoryCore/src/metadata/utils/id-generator.ts @@ -6,7 +6,7 @@ * - 关联关系表:UUID v4(`randomUUID()`) */ -import { randomUUID } from "node:crypto"; +import { randomInt, randomUUID } from "node:crypto"; const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"; const BASE = CHARS.length; // 36 @@ -43,7 +43,7 @@ function encodeBase36(value: number, length: number): string { function randomBase36(length: number): string { let out = ""; for (let i = 0; i < length; i++) { - out += CHARS[Math.floor(Math.random() * BASE)]; + out += CHARS[randomInt(BASE)]; } return out; } diff --git a/MemoryCore/src/offload/backend-client.ts b/MemoryCore/src/offload/backend-client.ts index 9d4957e512..61adfd1af7 100644 --- a/MemoryCore/src/offload/backend-client.ts +++ b/MemoryCore/src/offload/backend-client.ts @@ -13,6 +13,12 @@ import { traceOffloadModelIo } from "./opik-tracer.js"; import * as https from "node:https"; import * as http from "node:http"; +function stripTrailingSlashes(value: string): string { + let end = value.length; + while (end > 0 && value.charCodeAt(end - 1) === 47) end--; + return value.slice(0, end); +} + // ─── Request / Response Types ──────────────────────────────────────────────── export interface L1Request { @@ -126,7 +132,7 @@ export class BackendClient { userIdFn?: () => string | null, taskIdFn?: () => string | null, ) { - this.baseUrl = baseUrl.replace(/\/+$/, ""); + this.baseUrl = stripTrailingSlashes(baseUrl); this.apiKey = apiKey; this.logger = logger; this.sessionKeyFn = sessionKeyFn ?? (() => null); diff --git a/MemoryProxy/src/anthropicHandler.ts b/MemoryProxy/src/anthropicHandler.ts index 00e37a46f1..7bb31cb918 100644 --- a/MemoryProxy/src/anthropicHandler.ts +++ b/MemoryProxy/src/anthropicHandler.ts @@ -421,9 +421,9 @@ async function forwardWithRetry( const prefixEnd = anchorIdx >= 0 ? anchorIdx + 1 : msgs.length; const msgsPrefixStr = JSON.stringify(msgs.slice(0, prefixEnd)); - const sysFullMd5 = createHash("md5").update(sysFullStr).digest("hex").slice(0, 12); - const sysTextMd5 = createHash("md5").update(sysTextStr).digest("hex").slice(0, 12); - const msgsPrefixMd5 = createHash("md5").update(msgsPrefixStr).digest("hex").slice(0, 12); + const sysFullMd5 = createHash("sha256").update(sysFullStr).digest("hex").slice(0, 12); + const sysTextMd5 = createHash("sha256").update(sysTextStr).digest("hex").slice(0, 12); + const msgsPrefixMd5 = createHash("sha256").update(msgsPrefixStr).digest("hex").slice(0, 12); // eslint-disable-next-line no-console console.log( diff --git a/MemoryProxy/src/identity.ts b/MemoryProxy/src/identity.ts index 51d3330324..2697f55c1f 100644 --- a/MemoryProxy/src/identity.ts +++ b/MemoryProxy/src/identity.ts @@ -123,6 +123,9 @@ export function extractClientIdentity( } } + // Lookup / pseudonymization index for trace grouping, not password storage: + // the input is a high-entropy API key and nothing is persisted for comparison, + // so a fast hash is intentional and CWE-916 does not apply. const keyId = apiKey ? createHash("sha256").update(apiKey).digest("hex").slice(0, 8) : "unknown"; diff --git a/MemoryProxy/src/langfuse.ts b/MemoryProxy/src/langfuse.ts index 312e1cf9d9..933f56afcc 100644 --- a/MemoryProxy/src/langfuse.ts +++ b/MemoryProxy/src/langfuse.ts @@ -105,6 +105,9 @@ export async function shutdownLangfuse(): Promise { * 与官方 `createTraceId(seed)` 算法一致:SHA-256(seed) 的 hex 取前 32 位。 * 同一 turn 内每次请求都用相同 (sessionKey, turnSeq) → 得到相同 traceId → * 在 Langfuse 中归并到同一个 trace。 + * + * 这是 trace 归并用的确定性索引,不是密码存储/校验:输入为高熵 API key 派生的 + * sessionKey,且不落库做比对,因此使用快速哈希是有意为之,CWE-916 不适用。 */ export function langfuseTurnTraceId(sessionKey: string, turnSeq: number): string { const seed = `${sessionKey}:${turnSeq}`; diff --git a/MemoryProxy/src/opik.ts b/MemoryProxy/src/opik.ts index f8a8b6a52d..9f739126f1 100644 --- a/MemoryProxy/src/opik.ts +++ b/MemoryProxy/src/opik.ts @@ -33,7 +33,13 @@ function uuidv7(): string { return `${p1}-${p2}-${p3}-${p4}-${p5}`; } -/** Derive an 8-char key ID from an API key (SHA-256 first 8 hex chars). */ +/** Derive an 8-char key ID from an API key (SHA-256 first 8 hex chars). + * + * This is a lookup / pseudonymization index for trace grouping, not password + * storage or verification: the input is a high-entropy API key and nothing is + * persisted for later comparison. A fast hash is therefore intentional and + * CWE-916 (insufficient password hash) does not apply. + */ export function apiKeyToKeyId(apiKey: string): string { return createHash("sha256").update(apiKey).digest("hex").slice(0, 8); } diff --git a/sdk/memory-core/typescript/src/cos.ts b/sdk/memory-core/typescript/src/cos.ts index 80fe3e306a..01833eb877 100644 --- a/sdk/memory-core/typescript/src/cos.ts +++ b/sdk/memory-core/typescript/src/cos.ts @@ -17,6 +17,7 @@ */ import { TDAMError } from "./errors.js"; +import { stripTrailingSlashes } from "./http.js"; import { createHmac, createHash } from "node:crypto"; // ============================ @@ -123,7 +124,7 @@ export class StsCredentialManager { config: MemoryFileReaderConfig, bufferMs = 120_000, ) { - this.endpoint = config.endpoint.replace(/\/+$/, ""); + this.endpoint = stripTrailingSlashes(config.endpoint); this.apiKey = config.apiKey; this.serviceId = config.serviceId; this.timeout = config.timeout ?? 30_000; diff --git a/sdk/memory-core/typescript/src/http.ts b/sdk/memory-core/typescript/src/http.ts index 0f0ead6343..c110a73a2f 100644 --- a/sdk/memory-core/typescript/src/http.ts +++ b/sdk/memory-core/typescript/src/http.ts @@ -25,6 +25,12 @@ export interface HttpTransportOptions { rejectUnauthorized?: boolean; } +export function stripTrailingSlashes(value: string): string { + let end = value.length; + while (end > 0 && value.charCodeAt(end - 1) === 47) end--; + return value.slice(0, end); +} + export class HttpTransport { private readonly endpoint: string; private readonly headers: Record; @@ -32,7 +38,7 @@ export class HttpTransport { private dispatcher: unknown; constructor(opts: HttpTransportOptions) { - this.endpoint = opts.endpoint.replace(/\/+$/, ""); + this.endpoint = stripTrailingSlashes(opts.endpoint); this.timeout = opts.timeout ?? 30_000; this.headers = { Authorization: `Bearer ${opts.apiKey}`, diff --git a/sdk/memory-core/typescript/src/v3/http.ts b/sdk/memory-core/typescript/src/v3/http.ts index d6147c0c04..415e30ab64 100644 --- a/sdk/memory-core/typescript/src/v3/http.ts +++ b/sdk/memory-core/typescript/src/v3/http.ts @@ -2,7 +2,7 @@ import { Agent } from "undici"; import { ParamError, TDAMError } from "../errors.js"; -import type { HttpTransportOptions } from "../http.js"; +import { stripTrailingSlashes, type HttpTransportOptions } from "../http.js"; import type { ApiResponseEnvelope } from "../types.js"; export class V3HttpTransport { @@ -28,7 +28,7 @@ export class V3HttpTransport { throw new ParamError("timeout must be a positive number"); } - this.endpoint = opts.endpoint.replace(/\/+$/, ""); + this.endpoint = stripTrailingSlashes(opts.endpoint); this.timeout = timeout; this.headers = { Authorization: `Bearer ${opts.apiKey}`,