mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 18:18:37 +00:00
150 lines
5.8 KiB
TypeScript
150 lines
5.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, it, expect } from "vitest";
|
|
import { withEnvAsync } from "../test-utils/env.js";
|
|
import {
|
|
createConfigIO,
|
|
readConfigFileSnapshotForWrite,
|
|
writeConfigFile as writeConfigFileViaWrapper,
|
|
} from "./io.js";
|
|
|
|
async function withTempConfig(
|
|
configContent: string,
|
|
run: (configPath: string) => Promise<void>,
|
|
): Promise<void> {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-env-io-"));
|
|
const configPath = path.join(dir, "openclaw.json");
|
|
await fs.writeFile(configPath, configContent);
|
|
try {
|
|
await run(configPath);
|
|
} finally {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function withWrapperEnvContext(configPath: string, run: () => Promise<void>): Promise<void> {
|
|
await withEnvAsync(
|
|
{
|
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
OPENCLAW_DISABLE_CONFIG_CACHE: "1",
|
|
MY_API_KEY: "original-key-123",
|
|
},
|
|
run,
|
|
);
|
|
}
|
|
|
|
function createGatewayTokenConfigJson(): string {
|
|
return JSON.stringify({ gateway: { remote: { token: "${MY_API_KEY}" } } }, null, 2);
|
|
}
|
|
|
|
function createMutableApiKeyEnv(initialValue = "original-key-123"): Record<string, string> {
|
|
return { MY_API_KEY: initialValue };
|
|
}
|
|
|
|
async function withGatewayTokenTempConfig(
|
|
run: (configPath: string) => Promise<void>,
|
|
): Promise<void> {
|
|
await withTempConfig(createGatewayTokenConfigJson(), run);
|
|
}
|
|
|
|
async function withWrapperGatewayTokenContext(
|
|
run: (configPath: string) => Promise<void>,
|
|
): Promise<void> {
|
|
await withGatewayTokenTempConfig(async (configPath) => {
|
|
await withWrapperEnvContext(configPath, async () => run(configPath));
|
|
});
|
|
}
|
|
|
|
async function readGatewayToken(configPath: string): Promise<string> {
|
|
const written = await fs.readFile(configPath, "utf-8");
|
|
const parsed = JSON.parse(written) as { gateway: { remote: { token: string } } };
|
|
return parsed.gateway.remote.token;
|
|
}
|
|
|
|
describe("env snapshot TOCTOU via createConfigIO", () => {
|
|
it("restores env refs using read-time env even after env mutation", async () => {
|
|
const env = createMutableApiKeyEnv();
|
|
await withGatewayTokenTempConfig(async (configPath) => {
|
|
// Instance A: read config (captures env snapshot)
|
|
const ioA = createConfigIO({ configPath, env: env as unknown as NodeJS.ProcessEnv });
|
|
const firstRead = await ioA.readConfigFileSnapshotForWrite();
|
|
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
|
|
|
// Mutate env between read and write
|
|
env.MY_API_KEY = "mutated-key-456";
|
|
|
|
// Instance B: write config using explicit read context from A
|
|
const ioB = createConfigIO({ configPath, env: env as unknown as NodeJS.ProcessEnv });
|
|
|
|
// Write the resolved config back — should restore ${MY_API_KEY}
|
|
await ioB.writeConfigFile(firstRead.snapshot.config, firstRead.writeOptions);
|
|
|
|
// Verify the written file still has ${MY_API_KEY}, not the resolved value
|
|
const written = await fs.readFile(configPath, "utf-8");
|
|
const parsed = JSON.parse(written);
|
|
expect(parsed.gateway.remote.token).toBe("${MY_API_KEY}");
|
|
});
|
|
});
|
|
|
|
it("without snapshot bridging, mutated env causes incorrect restoration", async () => {
|
|
const env = createMutableApiKeyEnv();
|
|
await withGatewayTokenTempConfig(async (configPath) => {
|
|
// Instance A: read config
|
|
const ioA = createConfigIO({ configPath, env: env as unknown as NodeJS.ProcessEnv });
|
|
const snapshot = await ioA.readConfigFileSnapshot();
|
|
|
|
// Mutate env
|
|
env.MY_API_KEY = "mutated-key-456";
|
|
|
|
// Instance B: write WITHOUT snapshot bridging (simulates the old bug)
|
|
const ioB = createConfigIO({ configPath, env: env as unknown as NodeJS.ProcessEnv });
|
|
// No explicit writeOptions — ioB uses live env
|
|
|
|
await ioB.writeConfigFile(snapshot.config);
|
|
|
|
// The written file should have the raw value because the live env
|
|
// no longer matches — restoreEnvVarRefs won't find a match
|
|
const written = await fs.readFile(configPath, "utf-8");
|
|
const parsed = JSON.parse(written);
|
|
// Without snapshot, the resolved value "original-key-123" doesn't match
|
|
// live env "mutated-key-456", so restoration fails — value is written as-is
|
|
expect(parsed.gateway.remote.token).toBe("original-key-123");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("env snapshot TOCTOU via wrapper APIs", () => {
|
|
it("uses explicit read context even if another read interleaves", async () => {
|
|
await withWrapperGatewayTokenContext(async (configPath) => {
|
|
const firstRead = await readConfigFileSnapshotForWrite();
|
|
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
|
|
|
// Interleaving read from another request context with a different env value.
|
|
process.env.MY_API_KEY = "mutated-key-456";
|
|
const secondRead = await readConfigFileSnapshotForWrite();
|
|
expect(secondRead.snapshot.config.gateway?.remote?.token).toBe("mutated-key-456");
|
|
|
|
// Write using the first read's explicit context.
|
|
await writeConfigFileViaWrapper(firstRead.snapshot.config, firstRead.writeOptions);
|
|
expect(await readGatewayToken(configPath)).toBe("${MY_API_KEY}");
|
|
});
|
|
});
|
|
|
|
it("ignores read context when expected config path does not match", async () => {
|
|
await withWrapperGatewayTokenContext(async (configPath) => {
|
|
const firstRead = await readConfigFileSnapshotForWrite();
|
|
expect(firstRead.snapshot.config.gateway?.remote?.token).toBe("original-key-123");
|
|
expect(firstRead.writeOptions.expectedConfigPath).toBe(configPath);
|
|
|
|
process.env.MY_API_KEY = "mutated-key-456";
|
|
await writeConfigFileViaWrapper(firstRead.snapshot.config, {
|
|
...firstRead.writeOptions,
|
|
expectedConfigPath: `${configPath}.different`,
|
|
});
|
|
|
|
expect(await readGatewayToken(configPath)).toBe("original-key-123");
|
|
});
|
|
});
|
|
});
|