mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
fix(security): share outbound media source handling
This commit is contained in:
@@ -209,6 +209,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/failover: scope assistant-side fallback classification and surfaced provider errors to the current attempt instead of stale session history, so cross-provider fallback runs stop inheriting the previous provider's failure. (#62907) Thanks @stainlu.
|
||||
- MiniMax/OAuth: write `api: "anthropic-messages"` and `authHeader: true` into the `minimax-portal` config patch during `openclaw configure`, so re-authenticated portal setups keep Bearer auth routing working. (#64964) Thanks @ryanlee666.
|
||||
- Agents/tools: stop repeated unavailable-tool retries from escaping loop detection when the model changes arguments, and rewrite over-threshold unknown tool calls into plain assistant text before dispatch. (#65922) Thanks @dutifulbob.
|
||||
- Matrix/security: normalize sandboxed profile avatar params, preserve `mxc://` avatar URLs, and surface gmail watcher stop failures during reload. (#64701) Thanks @slepybear.
|
||||
|
||||
## 2026.4.10
|
||||
|
||||
|
||||
@@ -130,6 +130,16 @@ describe("resolveSandboxedMediaSource", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves remote mxc:// media sources", async () => {
|
||||
await withSandboxRoot(async (sandboxDir) => {
|
||||
const result = await resolveSandboxedMediaSource({
|
||||
media: "mxc://matrix.org/abc123def456",
|
||||
sandboxRoot: sandboxDir,
|
||||
});
|
||||
expect(result).toBe("mxc://matrix.org/abc123def456");
|
||||
});
|
||||
});
|
||||
|
||||
// Group 3: Rejections (security)
|
||||
it.each([
|
||||
{
|
||||
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
import { assertNoPathAliasEscape, type PathAliasPolicy } from "../infra/path-alias-guards.js";
|
||||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { isPassThroughRemoteMediaSource } from "../media/media-source-url.js";
|
||||
|
||||
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||
const HTTP_URL_RE = /^https?:\/\//i;
|
||||
const DATA_URL_RE = /^data:/i;
|
||||
const MXC_URL_RE = /^mxc:\/\//i;
|
||||
const SANDBOX_CONTAINER_WORKDIR = "/workspace";
|
||||
|
||||
function normalizeUnicodeSpaces(str: string): string {
|
||||
@@ -109,10 +108,7 @@ export async function resolveSandboxedMediaSource(params: {
|
||||
if (!raw) {
|
||||
return raw;
|
||||
}
|
||||
if (HTTP_URL_RE.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
if (MXC_URL_RE.test(raw)) {
|
||||
if (isPassThroughRemoteMediaSource(raw)) {
|
||||
return raw;
|
||||
}
|
||||
let candidate = raw;
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
collectActionMediaSourceHints,
|
||||
hydrateAttachmentParamsForAction,
|
||||
normalizeSandboxMediaList,
|
||||
normalizeSandboxMediaParams,
|
||||
@@ -129,6 +130,24 @@ describe("message action media helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("collects host media source hints from the shared media-source key set", () => {
|
||||
expect(
|
||||
collectActionMediaSourceHints({
|
||||
media: " /workspace/uploads/photo.png ",
|
||||
filePath: "",
|
||||
image: "file:///workspace/assets/event-cover.png",
|
||||
avatarPath: "/workspace/avatars/profile.png",
|
||||
avatar_url: "mxc://matrix.org/abc123def456",
|
||||
ignored: "/workspace/not-included.png",
|
||||
}),
|
||||
).toEqual([
|
||||
" /workspace/uploads/photo.png ",
|
||||
"file:///workspace/assets/event-cover.png",
|
||||
"/workspace/avatars/profile.png",
|
||||
"mxc://matrix.org/abc123def456",
|
||||
]);
|
||||
});
|
||||
|
||||
maybeIt("normalizes Matrix snake_case avatar_path and avatar_url aliases", async () => {
|
||||
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-avatar-snake-"));
|
||||
try {
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
import { extensionForMime } from "../../media/mime.js";
|
||||
import { loadWebMedia } from "../../media/web-media.js";
|
||||
import { readBooleanParam as readBooleanParamShared } from "../../plugin-sdk/boolean-param.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
|
||||
export const readBooleanParam = readBooleanParamShared;
|
||||
|
||||
const SANDBOX_MEDIA_PARAM_KEYS = [
|
||||
export const ACTION_MEDIA_SOURCE_PARAM_KEYS = [
|
||||
"media",
|
||||
"path",
|
||||
"filePath",
|
||||
@@ -31,11 +32,22 @@ const SANDBOX_MEDIA_PARAM_KEYS = [
|
||||
|
||||
function readMediaParam(
|
||||
args: Record<string, unknown>,
|
||||
key: (typeof SANDBOX_MEDIA_PARAM_KEYS)[number],
|
||||
key: (typeof ACTION_MEDIA_SOURCE_PARAM_KEYS)[number],
|
||||
): string | undefined {
|
||||
return readStringParam(args, key, { trim: false });
|
||||
}
|
||||
|
||||
export function collectActionMediaSourceHints(args: Record<string, unknown>): string[] {
|
||||
const sources: string[] = [];
|
||||
for (const key of ACTION_MEDIA_SOURCE_PARAM_KEYS) {
|
||||
const source = typeof args[key] === "string" ? args[key] : undefined;
|
||||
if (normalizeOptionalString(source)) {
|
||||
sources.push(source);
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
function readAttachmentMediaHint(args: Record<string, unknown>): string | undefined {
|
||||
return readMediaParam(args, "media") ?? readMediaParam(args, "mediaUrl");
|
||||
}
|
||||
@@ -236,7 +248,7 @@ export async function normalizeSandboxMediaParams(params: {
|
||||
}): Promise<void> {
|
||||
const sandboxRoot =
|
||||
params.mediaPolicy.mode === "sandbox" ? params.mediaPolicy.sandboxRoot.trim() : undefined;
|
||||
for (const key of SANDBOX_MEDIA_PARAM_KEYS) {
|
||||
for (const key of ACTION_MEDIA_SOURCE_PARAM_KEYS) {
|
||||
const raw = readMediaParam(params.args, key);
|
||||
if (!raw) {
|
||||
continue;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
import type { OutboundSendDeps } from "./deliver.js";
|
||||
import { normalizeMessageActionInput } from "./message-action-normalization.js";
|
||||
import {
|
||||
collectActionMediaSourceHints,
|
||||
hydrateAttachmentParamsForAction,
|
||||
normalizeSandboxMediaList,
|
||||
normalizeSandboxMediaParams,
|
||||
@@ -203,19 +204,6 @@ async function resolveGatewayActionIdempotencyKey(idempotencyKey?: string): Prom
|
||||
const { randomIdempotencyKey } = await loadMessageActionGatewayRuntime();
|
||||
return randomIdempotencyKey();
|
||||
}
|
||||
|
||||
function collectActionMediaSourceHints(params: Record<string, unknown>): string[] {
|
||||
const sources: string[] = [];
|
||||
for (const key of ["media", "mediaUrl", "path", "filePath", "fileUrl", "image", "avatarPath", "avatar_path", "avatarUrl", "avatar_url"] as const) {
|
||||
const source = typeof params[key] === "string" ? params[key] : undefined;
|
||||
const normalized = normalizeOptionalString(source);
|
||||
if (normalized && source) {
|
||||
sources.push(source);
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
function applyCrossContextMessageDecoration({
|
||||
params,
|
||||
message,
|
||||
|
||||
7
src/media/media-source-url.ts
Normal file
7
src/media/media-source-url.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
const HTTP_URL_RE = /^https?:\/\//i;
|
||||
const MXC_URL_RE = /^mxc:\/\//i;
|
||||
|
||||
export function isPassThroughRemoteMediaSource(value: string | null | undefined): boolean {
|
||||
const normalized = value?.trim() ?? "";
|
||||
return Boolean(normalized) && (HTTP_URL_RE.test(normalized) || MXC_URL_RE.test(normalized));
|
||||
}
|
||||
Reference in New Issue
Block a user