Skip to content

Commit af23a2e

Browse files
committed
test(agents): cover C1-boundary truncation in model warning value
Add a warning-level regression driving the real resolveConfiguredModelRef providerless-model path: a value with a residual C1 control byte (U+0080) followed by visible text truncates the logged warning at the C1 boundary ("gpt4") instead of retaining the attacker-controlled suffix, and a clean value is preserved. Locks down the new C1 branch in sanitizeModelWarningValue. Signed-off-by: lsr911 <[email protected]>
1 parent 4b72d62 commit af23a2e

1 file changed

Lines changed: 51 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Regression tests for sanitizeModelWarningValue's C1 boundary truncation,
2+
// exercised through the exported resolveConfiguredModelRef warning path.
3+
//
4+
// A providerless model value that contains a residual C1 control byte (one that
5+
// survives ANSI stripping, e.g. U+0080) must have the providerless-model
6+
// warning truncated at that byte, so trailing attacker-controlled text after a
7+
// C1 control cannot ride along into the logged warning.
8+
import { describe, expect, it, vi } from "vitest";
9+
import type { OpenClawConfig } from "../config/types.openclaw.js";
10+
11+
const { warnMock } = vi.hoisted(() => ({ warnMock: vi.fn() }));
12+
13+
vi.mock("../logging/subsystem.js", () => ({
14+
createSubsystemLogger: () => ({
15+
warn: warnMock,
16+
info: vi.fn(),
17+
debug: vi.fn(),
18+
error: vi.fn(),
19+
}),
20+
}));
21+
22+
const { resolveConfiguredModelRef } = await import("./model-selection-shared.js");
23+
24+
function warnFor(model: string): string {
25+
warnMock.mockClear();
26+
const cfg = { agents: { defaults: { model } } } as unknown as OpenClawConfig;
27+
const ref = resolveConfiguredModelRef({
28+
cfg,
29+
defaultProvider: "openai",
30+
defaultModel: "gpt-5",
31+
});
32+
// The resolved ref preserves the raw model; only the logged warning is sanitized.
33+
expect(ref).toEqual({ provider: "openai", model });
34+
return String(warnMock.mock.calls.at(-1)?.[0] ?? "");
35+
}
36+
37+
describe("sanitizeModelWarningValue C1 boundary", () => {
38+
it("truncates the providerless-model warning at a residual C1 byte", () => {
39+
const PAD = String.fromCharCode(0x80); // C1 control that survives ANSI stripping
40+
const warn = warnFor(`gpt4${PAD}EVIL`);
41+
// Warning is truncated at the C1 boundary: the visible suffix is dropped.
42+
expect(warn).toContain('Model "gpt4" specified without provider');
43+
expect(warn).not.toContain("EVIL");
44+
expect(warn).not.toContain(PAD);
45+
});
46+
47+
it("keeps a clean providerless-model value intact in the warning", () => {
48+
const warn = warnFor("gpt4");
49+
expect(warn).toContain('Model "gpt4" specified without provider');
50+
});
51+
});

0 commit comments

Comments
 (0)