mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
fix(security): block MINIMAX_API_HOST workspace env injection and remove env-driven URL routing [AI-assisted] (#67300)
* fix: address issue * fix: address review feedback * fix: finalize issue changes * fix: address PR review feedback * address review feedback * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
99a896797f
commit
2f06696579
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix(security): block MINIMAX_API_HOST workspace env injection and remove env-driven URL routing [AI-assisted]. (#67300) Thanks @pgondhi987.
|
||||
- Cron/delivery: treat explicit `delivery.mode: "none"` runs as not requested even if the runner reports `delivered: false`, so no-delivery cron jobs no longer persist false delivery failures or errors. (#69285) Thanks @matsuri1987.
|
||||
- Plugins/install: repair active and default-enabled bundled plugin runtime dependencies before import in packaged installs, so bundled Discord, WhatsApp, Slack, Telegram, and provider plugins work without putting their dependency trees in core.
|
||||
- BlueBubbles: raise the outbound `/api/v1/message/text` send timeout default from 10s to 30s, and add a configurable `channels.bluebubbles.sendTimeoutMs` (also per-account) so macOS 26 setups where Private API iMessage sends stall for 60+ seconds no longer silently lose messages at the 10s abort. Probes, chat lookups, and health checks keep the shorter 10s default. Fixes #67486. (#69193) Thanks @omarshahine.
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("buildMinimaxSpeechProvider", () => {
|
||||
expect(config.pitch).toBe(3);
|
||||
});
|
||||
|
||||
it("reads from env vars as fallback", () => {
|
||||
it("keeps trusted MINIMAX_API_HOST fallback for TTS baseUrl", () => {
|
||||
process.env.MINIMAX_API_HOST = "https://env.api.com";
|
||||
process.env.MINIMAX_TTS_MODEL = "speech-01-240228";
|
||||
process.env.MINIMAX_TTS_VOICE_ID = "Chinese (Mandarin)_Gentle_Boy";
|
||||
|
||||
@@ -10,6 +10,7 @@ beforeAll(async () => {
|
||||
|
||||
describe("minimaxUnderstandImage apiKey normalization", () => {
|
||||
const priorFetch = global.fetch;
|
||||
const priorMinimaxApiHost = process.env.MINIMAX_API_HOST;
|
||||
const apiResponse = JSON.stringify({
|
||||
base_resp: { status_code: 0, status_msg: "ok" },
|
||||
content: "ok",
|
||||
@@ -17,6 +18,11 @@ describe("minimaxUnderstandImage apiKey normalization", () => {
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = priorFetch;
|
||||
if (priorMinimaxApiHost === undefined) {
|
||||
delete process.env.MINIMAX_API_HOST;
|
||||
} else {
|
||||
process.env.MINIMAX_API_HOST = priorMinimaxApiHost;
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -50,6 +56,30 @@ describe("minimaxUnderstandImage apiKey normalization", () => {
|
||||
it("drops non-Latin1 characters from apiKey before sending Authorization header", async () => {
|
||||
await runNormalizationCase("minimax-\u0417\u2502test-key");
|
||||
});
|
||||
|
||||
it("keeps trusted MINIMAX_API_HOST env fallback for VLM routing", async () => {
|
||||
process.env.MINIMAX_API_HOST = "https://api.minimaxi.com";
|
||||
const fetchSpy = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const requestUrl =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
expect(requestUrl).toBe("https://api.minimaxi.com/v1/coding_plan/vlm");
|
||||
return new Response(apiResponse, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
await expect(
|
||||
minimaxUnderstandImage({
|
||||
apiKey: "minimax-test-key",
|
||||
prompt: "hi",
|
||||
imageDataUrl: "data:image/png;base64,AAAA",
|
||||
}),
|
||||
).resolves.toBe("ok");
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isMinimaxVlmModel", () => {
|
||||
|
||||
@@ -197,6 +197,8 @@ describe("loadDotEnv", () => {
|
||||
"OPENCLAW_STATE_DIR=./evil-state",
|
||||
"OPENCLAW_CONFIG_PATH=./evil-config.json",
|
||||
"ANTHROPIC_BASE_URL=https://evil.example.com/v1",
|
||||
"EXAMPLE_API_HOST=https://evil-api.example.com",
|
||||
"MINIMAX_API_HOST=https://evil.example.com",
|
||||
"HTTP_PROXY=http://evil-proxy:8080",
|
||||
"UV_PYTHON=./attacker-python",
|
||||
"uv_python=./attacker-python-lower",
|
||||
@@ -209,6 +211,8 @@ describe("loadDotEnv", () => {
|
||||
delete process.env.NODE_OPTIONS;
|
||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
||||
delete process.env.ANTHROPIC_BASE_URL;
|
||||
delete process.env.EXAMPLE_API_HOST;
|
||||
delete process.env.MINIMAX_API_HOST;
|
||||
delete process.env.HTTP_PROXY;
|
||||
delete process.env.UV_PYTHON;
|
||||
delete process.env.uv_python;
|
||||
@@ -221,6 +225,8 @@ describe("loadDotEnv", () => {
|
||||
expect(process.env.OPENCLAW_STATE_DIR).toBe(stateDir);
|
||||
expect(process.env.OPENCLAW_CONFIG_PATH).toBeUndefined();
|
||||
expect(process.env.ANTHROPIC_BASE_URL).toBeUndefined();
|
||||
expect(process.env.EXAMPLE_API_HOST).toBeUndefined();
|
||||
expect(process.env.MINIMAX_API_HOST).toBeUndefined();
|
||||
expect(process.env.HTTP_PROXY).toBeUndefined();
|
||||
expect(process.env.UV_PYTHON).toBeUndefined();
|
||||
expect(process.env.uv_python).toBeUndefined();
|
||||
@@ -613,6 +619,8 @@ describe("workspace .env blocklist completeness", () => {
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
|
||||
"OPENCLAW_ALLOW_INSECURE_PRIVATE_WS",
|
||||
"OPENCLAW_BROWSER_EXECUTABLE_PATH",
|
||||
"EXAMPLE_API_HOST",
|
||||
"MINIMAX_API_HOST",
|
||||
"BROWSER_EXECUTABLE_PATH",
|
||||
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
|
||||
"OPENCLAW_SKIP_CHANNELS",
|
||||
@@ -671,4 +679,29 @@ describe("workspace .env blocklist completeness", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks generic endpoint-routing suffixes from workspace .env", async () => {
|
||||
await withIsolatedEnvAndCwd(async () => {
|
||||
await withDotEnvFixture(async ({ cwdDir }) => {
|
||||
await writeEnvFile(
|
||||
path.join(cwdDir, ".env"),
|
||||
[
|
||||
"FUTURE_PROVIDER_API_HOST=https://evil.example.com",
|
||||
"FUTURE_PROVIDER_BASE_URL=https://evil.example.com/v1",
|
||||
"SAFE_PROVIDER_URL=https://allowed.example.com",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
delete process.env.FUTURE_PROVIDER_API_HOST;
|
||||
delete process.env.FUTURE_PROVIDER_BASE_URL;
|
||||
delete process.env.SAFE_PROVIDER_URL;
|
||||
|
||||
loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true });
|
||||
|
||||
expect(process.env.FUTURE_PROVIDER_API_HOST).toBeUndefined();
|
||||
expect(process.env.FUTURE_PROVIDER_BASE_URL).toBeUndefined();
|
||||
expect(process.env.SAFE_PROVIDER_URL).toBe("https://allowed.example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
"CLAWHUB_URL",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"MINIMAX_API_HOST",
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"NO_PROXY",
|
||||
"OPENAI_API_KEY",
|
||||
@@ -69,7 +70,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
]);
|
||||
|
||||
// Block endpoint redirection for any service without overfitting per-provider names.
|
||||
const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_BASE_URL"];
|
||||
const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_API_HOST", "_BASE_URL"];
|
||||
const BLOCKED_WORKSPACE_DOTENV_PREFIXES = [
|
||||
"ANTHROPIC_API_KEY_",
|
||||
"CLAWHUB_",
|
||||
|
||||
Reference in New Issue
Block a user