mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 03:31:46 +00:00
fix(security): preserve UTF-16 token mask boundaries (#103843)
This commit is contained in:
committed by
GitHub
parent
8a93d288e2
commit
c8ebfd8a3a
@@ -3,6 +3,9 @@
|
||||
"version": "0.0.0-private",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@openclaw/normalization-core": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
"./runtime": "./src/runtime.ts",
|
||||
"./runtime-core": "./src/runtime-core.ts",
|
||||
|
||||
23
packages/memory-host-sdk/src/host/error-utils.test.ts
Normal file
23
packages/memory-host-sdk/src/host/error-utils.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Memory Host SDK tests cover error formatting and secret redaction.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatErrorMessage } from "./error-utils.js";
|
||||
|
||||
const TOKEN_CASES = [
|
||||
["leading split", "abcde😀xxxxxxxxwxyz", "abcde...wxyz"],
|
||||
["trailing split", "abcdefghijklm😀xyz", "abcdef...xyz"],
|
||||
["intact leading pair", "abcd😀xxxxxxxxwxyz", "abcd😀...wxyz"],
|
||||
["intact trailing pair", "abcdefghijklmn😀xy", "abcdef...😀xy"],
|
||||
] as const;
|
||||
|
||||
describe("formatErrorMessage", () => {
|
||||
it.each(TOKEN_CASES)("masks tokens with a UTF-16-safe %s", (_label, token, masked) => {
|
||||
expect(formatErrorMessage(`TOKEN=${token}`)).toBe(`TOKEN=${masked}`);
|
||||
});
|
||||
|
||||
it("replaces the captured value literally when key and value repeat", () => {
|
||||
expect(formatErrorMessage("LONG_LONG_LONG_TOKEN=LONG_LONG_LONG_TOKEN")).toBe(
|
||||
"LONG_LONG_LONG_TOKEN=LONG_L...OKEN",
|
||||
);
|
||||
expect(formatErrorMessage("TOKEN=$&abcdxxxxxxxxwxyz")).toBe("TOKEN=$&abcd...wxyz");
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,14 @@ function redactMatch(match: string, groups: string[]): string {
|
||||
}
|
||||
const token = groups.findLast((value) => typeof value === "string" && value.length > 0) ?? match;
|
||||
const masked = maskToken(token);
|
||||
return token === match ? masked : match.replace(token, masked);
|
||||
if (token === match) {
|
||||
return masked;
|
||||
}
|
||||
const tokenOffset = match.lastIndexOf(token);
|
||||
if (tokenOffset < 0) {
|
||||
return "***";
|
||||
}
|
||||
return `${match.slice(0, tokenOffset)}${masked}${match.slice(tokenOffset + token.length)}`;
|
||||
}
|
||||
|
||||
function redactSensitiveText(text: string): string {
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -1953,7 +1953,11 @@ importers:
|
||||
|
||||
packages/media-understanding-common: {}
|
||||
|
||||
packages/memory-host-sdk: {}
|
||||
packages/memory-host-sdk:
|
||||
dependencies:
|
||||
'@openclaw/normalization-core':
|
||||
specifier: workspace:*
|
||||
version: link:../normalization-core
|
||||
|
||||
packages/model-catalog-core: {}
|
||||
|
||||
|
||||
@@ -55,4 +55,36 @@ describe("browser tool detail redaction", () => {
|
||||
"OPENAI_API_KEY=sk-123...cdef",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["leading split", "abcde😀xxxxxxxxwxyz", "abcde...wxyz"],
|
||||
["trailing split", "abcdefghijklm😀xyz", "abcdef...xyz"],
|
||||
["intact leading pair", "abcd😀xxxxxxxxwxyz", "abcd😀...wxyz"],
|
||||
["intact trailing pair", "abcdefghijklmn😀xy", "abcdef...😀xy"],
|
||||
])("masks tool payload tokens with a UTF-16-safe %s", (_label, token, masked) => {
|
||||
expect(redactToolPayloadText(`{"token":"${token}"}`)).toBe(`{"token":"${masked}"}`);
|
||||
});
|
||||
|
||||
it("does not trust mask-shaped input as already redacted", () => {
|
||||
expect(redactToolPayloadText("TOKEN=abcde...wxyz")).toBe("TOKEN=abcde....wxyz");
|
||||
});
|
||||
|
||||
it("redacts replacement-template text literally without changing surrounding text", () => {
|
||||
const markerLike = "\u{e000}0\u{e001}";
|
||||
expect(redactToolPayloadText(`${markerLike} TOKEN=$\`abcdxxxxxxxxwxyz`)).toBe(
|
||||
`${markerLike} TOKEN=$\`abcd...wxyz`,
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts the captured value when key and value repeat", () => {
|
||||
expect(redactToolPayloadText("LONG_LONG_LONG_TOKEN=LONG_LONG_LONG_TOKEN")).toBe(
|
||||
"LONG_LONG_LONG_TOKEN=LONG_L...OKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers an outer credential match over a nested one", () => {
|
||||
expect(redactToolPayloadText('cookie: "TOKEN=abcdefghijklmno&abcdefghij"')).toBe(
|
||||
'cookie: "TOKEN=...ghij"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Browser-safe redaction for tool details rendered by the Control UI.
|
||||
import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
|
||||
const PAYMENT_CREDENTIAL_KEYS =
|
||||
"card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|securityCode|payment[-_]?credential|paymentCredential|shared[-_]?payment[-_]?token|sharedPaymentToken";
|
||||
|
||||
@@ -52,7 +54,9 @@ function redactToken(value: string): string {
|
||||
if (value.length <= 10) {
|
||||
return "***";
|
||||
}
|
||||
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
||||
// A 6/4-shaped hint is intentionally idempotent: every non-separator
|
||||
// character is already inside the visible prefix or suffix budget.
|
||||
return `${sliceUtf16Safe(value, 0, 6)}...${sliceUtf16Safe(value, -4)}`;
|
||||
}
|
||||
|
||||
function redactPemBlock(block: string): string {
|
||||
@@ -63,18 +67,28 @@ function redactPemBlock(block: string): string {
|
||||
return `${lines[0]}\n...redacted...\n${lines[lines.length - 1]}`;
|
||||
}
|
||||
|
||||
function redactMatch(match: string, groups: Array<string | undefined>): string {
|
||||
if (match.includes("PRIVATE KEY-----")) {
|
||||
return redactPemBlock(match);
|
||||
}
|
||||
const token = groups.findLast((group) => typeof group === "string" && group.length > 0) ?? match;
|
||||
const masked = redactToken(token);
|
||||
if (token === match) {
|
||||
return masked;
|
||||
}
|
||||
const tokenOffset = match.lastIndexOf(token);
|
||||
if (tokenOffset < 0) {
|
||||
return "***";
|
||||
}
|
||||
return `${match.slice(0, tokenOffset)}${masked}${match.slice(tokenOffset + token.length)}`;
|
||||
}
|
||||
|
||||
export function redactToolDetail(detail: string): string {
|
||||
let redacted = detail;
|
||||
for (const pattern of SECRET_DETAIL_PATTERNS) {
|
||||
redacted = redacted.replace(pattern, (...args: string[]) => {
|
||||
const match = args[0] ?? "";
|
||||
if (match.includes("PRIVATE KEY-----")) {
|
||||
return redactPemBlock(match);
|
||||
}
|
||||
const groups = args.slice(1, -2);
|
||||
const token = groups.findLast((group) => typeof group === "string" && group.length > 0);
|
||||
return token ? match.replace(token, redactToken(token)) : "***";
|
||||
});
|
||||
redacted = redacted.replace(pattern, (...args: string[]) =>
|
||||
redactMatch(args[0] ?? "", args.slice(1, -2)),
|
||||
);
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user