mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(signal): reject malformed base64 attachment data (#114883)
signalRpcRequest returns the JSON-RPC result via a bare cast, so getAttachment data reaches Buffer.from unvalidated. Node drops out-of-alphabet characters instead of throwing, so a damaged payload was silently written to disk as a corrupted attachment. Canonicalize after the existing size guard and fail with the attachment id instead.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SignalEventHandlerDeps } from "./monitor/event-handler.types.js";
|
||||
|
||||
const signalRpcRequestMock = vi.hoisted(() => vi.fn());
|
||||
const saveMediaBufferMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
let capturedFetchAttachment: SignalEventHandlerDeps["fetchAttachment"] | undefined;
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/media-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/media-runtime")>(
|
||||
"openclaw/plugin-sdk/media-runtime",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
saveMediaBuffer: saveMediaBufferMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./client-adapter.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./client-adapter.js")>("./client-adapter.js");
|
||||
return {
|
||||
...actual,
|
||||
signalRpcRequest: signalRpcRequestMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./monitor/event-handler.js", () => ({
|
||||
createSignalEventHandler: (deps: SignalEventHandlerDeps) => {
|
||||
capturedFetchAttachment = deps.fetchAttachment;
|
||||
return async () => {};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./signal-ingress.js", () => ({
|
||||
startSignalIngressMonitor: async () => ({
|
||||
receive: async () => {},
|
||||
stop: async () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./sse-reconnect.js", () => ({
|
||||
runSignalSseLoop: async () => {},
|
||||
}));
|
||||
|
||||
const { monitorSignalProvider } = await import("./monitor.js");
|
||||
|
||||
const config = {
|
||||
channels: {
|
||||
signal: {
|
||||
transport: { kind: "external-native", url: "http://127.0.0.1:8080" },
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
function requireCapturedFetchAttachment(): SignalEventHandlerDeps["fetchAttachment"] {
|
||||
if (!capturedFetchAttachment) {
|
||||
throw new Error("expected monitor to configure fetchAttachment");
|
||||
}
|
||||
return capturedFetchAttachment;
|
||||
}
|
||||
|
||||
describe("Signal attachment fetch", () => {
|
||||
beforeEach(() => {
|
||||
capturedFetchAttachment = undefined;
|
||||
signalRpcRequestMock.mockReset();
|
||||
saveMediaBufferMock.mockReset().mockResolvedValue({
|
||||
path: "/tmp/signal-attachment.png",
|
||||
contentType: "image/png",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed base64 attachment data", async () => {
|
||||
signalRpcRequestMock.mockResolvedValue({
|
||||
data: "iVBORw0KGgoAAAANSUhE%%%UgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2Nk+M/wHwAF/gL+M6Q10QAAAABJRU5ErkJggg==",
|
||||
});
|
||||
|
||||
await monitorSignalProvider({ config, autoStart: false });
|
||||
const fetchAttachment = requireCapturedFetchAttachment();
|
||||
|
||||
await expect(
|
||||
fetchAttachment({
|
||||
baseUrl: "http://127.0.0.1:8080",
|
||||
attachment: { id: "attachment-123", contentType: "image/png" },
|
||||
sender: "+15550001111",
|
||||
maxBytes: 8 * 1024 * 1024,
|
||||
}),
|
||||
).rejects.toThrow("Signal attachment attachment-123 returned malformed base64 data");
|
||||
expect(saveMediaBufferMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
SignalReactionNotificationMode,
|
||||
} from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
canonicalizeBase64,
|
||||
detectMime,
|
||||
estimateBase64DecodedBytes,
|
||||
saveMediaBuffer,
|
||||
@@ -315,7 +316,11 @@ async function fetchAttachment(params: {
|
||||
`Signal attachment ${attachment.id} exceeds ${(params.maxBytes / (1024 * 1024)).toFixed(0)}MB limit`,
|
||||
);
|
||||
}
|
||||
const buffer = Buffer.from(result.data, "base64");
|
||||
const canonicalData = canonicalizeBase64(result.data);
|
||||
if (!canonicalData) {
|
||||
throw new Error(`Signal attachment ${attachment.id} returned malformed base64 data`);
|
||||
}
|
||||
const buffer = Buffer.from(canonicalData, "base64");
|
||||
const originalFilename = normalizeOptionalString(attachment.filename ?? undefined);
|
||||
const contentType =
|
||||
normalizeOptionalString(attachment.contentType ?? undefined) ??
|
||||
|
||||
Reference in New Issue
Block a user