Skip to content

Commit e949737

Browse files
Alix-007Leon-SK668
authored andcommitted
fix(signal): validate release metadata before install
1 parent 25d6f13 commit e949737

2 files changed

Lines changed: 53 additions & 4 deletions

File tree

extensions/signal/src/install-signal-cli.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,26 @@ describe("installSignalCliFromRelease", () => {
390390
expect(fetchResult.release).toHaveBeenCalledTimes(1);
391391
});
392392

393+
it.each([
394+
["null", "null"],
395+
["array", "[]"],
396+
["non-string tag_name", JSON.stringify({ tag_name: 123, assets: [] })],
397+
["non-array assets", JSON.stringify({ tag_name: "v0.14.6", assets: {} })],
398+
])("returns an installer error for a valid JSON %s payload", async (_kind, body) => {
399+
const fetchResult = okDownloadResponse(body, {
400+
headers: { "content-type": "application/json" },
401+
});
402+
fetchWithSsrFGuardMock.mockResolvedValue(fetchResult);
403+
404+
const result = await installSignalCliFromRelease({ log: vi.fn() } as unknown as RuntimeEnv);
405+
406+
expect(result).toEqual({
407+
ok: false,
408+
error: "Failed to parse signal-cli release info.",
409+
});
410+
expect(fetchResult.release).toHaveBeenCalledTimes(1);
411+
});
412+
393413
it("bounds oversized GitHub release metadata and cancels the stream", async () => {
394414
const chunkSize = 1024 * 1024;
395415
const chunkCount = 20; // 20 MiB — over the 16 MiB cap
@@ -517,13 +537,16 @@ describe("installSignalCliFromRelease", () => {
517537
}
518538
});
519539

520-
it("removes the download temp dir when the download throws", async () => {
540+
it("skips malformed asset rows while retaining a valid download", async () => {
521541
setProcessPlatform("linux", "x64");
522542
fetchWithSsrFGuardMock.mockResolvedValueOnce(
523543
okDownloadResponse(
524544
JSON.stringify({
525545
tag_name: "v0.0.0-download-failure-test",
526546
assets: [
547+
null,
548+
{ name: 42, browser_download_url: "https://example.com/wrong-name.tar.gz" },
549+
{ name: "signal-cli-wrong-url.tar.gz", browser_download_url: false },
527550
{
528551
name: "signal-cli-0.0.0-Linux-native.tar.gz",
529552
browser_download_url: "https://example.com/linux-native.tar.gz",
@@ -539,6 +562,10 @@ describe("installSignalCliFromRelease", () => {
539562
installSignalCliFromRelease({ log: vi.fn() } as unknown as RuntimeEnv),
540563
).rejects.toThrow("download failed");
541564

565+
expect(fetchWithSsrFGuardMock).toHaveBeenNthCalledWith(
566+
2,
567+
expect.objectContaining({ url: "https://example.com/linux-native.tar.gz" }),
568+
);
542569
await expectTempDownloadDirMissing();
543570
});
544571
});

extensions/signal/src/install-signal-cli.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import path from "node:path";
55
import { Readable, Transform } from "node:stream";
66
import { pipeline } from "node:stream/promises";
77
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
8-
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
8+
import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http";
99
import { runPluginCommandWithTimeout } from "openclaw/plugin-sdk/run-command";
1010
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
1111
import { CONFIG_DIR, extractArchive, resolveBrewExecutable } from "openclaw/plugin-sdk/setup-tools";
1212
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
13-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
13+
import {
14+
isRecord,
15+
normalizeLowercaseStringOrEmpty,
16+
} from "openclaw/plugin-sdk/string-coerce-runtime";
1417
import { withTempDownloadPath } from "openclaw/plugin-sdk/temp-path";
1518
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
1619

@@ -325,7 +328,26 @@ export async function installSignalCliFromRelease(
325328
};
326329
}
327330
try {
328-
payload = await readProviderJsonResponse<ReleaseResponse>(response, "signal.release-info");
331+
const releaseInfo = await readProviderJsonObjectResponse(response, "signal.release-info");
332+
const tagName = releaseInfo.tag_name;
333+
if (tagName !== undefined && typeof tagName !== "string") {
334+
throw new Error("Unexpected signal-cli release info");
335+
}
336+
const releaseAssets = releaseInfo.assets;
337+
if (releaseAssets !== undefined && !Array.isArray(releaseAssets)) {
338+
throw new Error("Unexpected signal-cli release info");
339+
}
340+
const assets = (releaseAssets ?? []).flatMap((asset): ReleaseAsset[] => {
341+
if (
342+
!isRecord(asset) ||
343+
typeof asset.name !== "string" ||
344+
typeof asset.browser_download_url !== "string"
345+
) {
346+
return [];
347+
}
348+
return [{ name: asset.name, browser_download_url: asset.browser_download_url }];
349+
});
350+
payload = { tag_name: tagName, assets };
329351
} catch {
330352
return {
331353
ok: false,

0 commit comments

Comments
 (0)