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:
Pavan Kumar Gondhi
2026-04-20 22:51:03 +05:30
committed by GitHub
parent 99a896797f
commit 2f06696579
5 changed files with 67 additions and 2 deletions

View File

@@ -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.

View File

@@ -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";

View File

@@ -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", () => {

View File

@@ -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");
});
});
});
});

View File

@@ -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_",