From bc6d570659f7504cb19ce4382ecda7cf66ae441d Mon Sep 17 00:00:00 2001 From: Shakker Date: Sat, 30 May 2026 13:22:04 +0100 Subject: [PATCH] fix: reject non-text skill proposal files --- src/cli/skills-cli.ts | 6 +++--- src/skills/workshop/service.test.ts | 22 ++++++++++++++++++++++ src/skills/workshop/service.ts | 22 +++++++++++++++++++--- src/skills/workshop/store.ts | 3 +++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index 0f2843f1ce73..1183af9c8438 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -541,7 +541,7 @@ export function registerSkillsCli(program: Command) { .option("--proposal ", "Path to PROPOSAL.md draft content") .option( "--proposal-dir ", - "Path to proposal directory with PROPOSAL.md and support files", + "Path to proposal directory with PROPOSAL.md and UTF-8 text support files", ) .option("--goal ", "Research or improvement goal") .option("--evidence ", "Evidence or notes for the proposal") @@ -592,7 +592,7 @@ export function registerSkillsCli(program: Command) { .option("--proposal ", "Path to PROPOSAL.md draft content") .option( "--proposal-dir ", - "Path to proposal directory with PROPOSAL.md and support files", + "Path to proposal directory with PROPOSAL.md and UTF-8 text support files", ) .option("--goal ", "Research or improvement goal") .option("--evidence ", "Evidence or notes for the proposal") @@ -646,7 +646,7 @@ export function registerSkillsCli(program: Command) { .option("--proposal ", "Path to revised PROPOSAL.md draft content") .option( "--proposal-dir ", - "Path to revised proposal directory with PROPOSAL.md and support files", + "Path to revised proposal directory with PROPOSAL.md and UTF-8 text support files", ) .option("--description ", "Replacement proposal description") .option("--goal ", "Replacement research or improvement goal") diff --git a/src/skills/workshop/service.test.ts b/src/skills/workshop/service.test.ts index 224ba5150cc8..b4f1456d3594 100644 --- a/src/skills/workshop/service.test.ts +++ b/src/skills/workshop/service.test.ts @@ -12,6 +12,7 @@ import { proposeCreateSkill, proposeUpdateSkill, quarantineSkillProposal, + readSkillProposalDraftDirectory, rejectSkillProposal, resolvePendingSkillProposal, reviseSkillProposal, @@ -618,6 +619,27 @@ describe("skill workshop proposals", () => { await expect(fs.access(path.join(stateDir, "skill-workshop"))).rejects.toThrow(); }); + it("rejects non-text and executable proposal directory support files", async () => { + const draftDir = path.join(await makeWorkspace(), "draft"); + await fs.mkdir(path.join(draftDir, "assets"), { recursive: true }); + await fs.writeFile(path.join(draftDir, "PROPOSAL.md"), "# Binary Asset\n", "utf8"); + await fs.writeFile(path.join(draftDir, "assets", "icon.png"), Buffer.from([0x89, 0x50])); + + await expect(readSkillProposalDraftDirectory(draftDir)).rejects.toThrow( + "Proposal files must be UTF-8 text", + ); + + await fs.rm(path.join(draftDir, "assets", "icon.png")); + await fs.mkdir(path.join(draftDir, "scripts"), { recursive: true }); + const scriptPath = path.join(draftDir, "scripts", "run.sh"); + await fs.writeFile(scriptPath, "#!/bin/sh\necho ok\n", "utf8"); + await fs.chmod(scriptPath, 0o755); + + await expect(readSkillProposalDraftDirectory(draftDir)).rejects.toThrow( + "Proposal support files must not be executable", + ); + }); + it("rejects rendered proposals that exceed the persisted draft size limit", async () => { const workspaceDir = await makeWorkspace(); diff --git a/src/skills/workshop/service.ts b/src/skills/workshop/service.ts index 8b7c87fdb52f..0246fbbf3ca7 100644 --- a/src/skills/workshop/service.ts +++ b/src/skills/workshop/service.ts @@ -1,3 +1,4 @@ +import fs from "node:fs/promises"; import path from "node:path"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { readLocalFileSafely, root, walkDirectory } from "../../infra/fs-safe.js"; @@ -92,7 +93,7 @@ export async function readSkillProposalDraftFile(filePath: string): Promise MAX_PROPOSAL_SUPPORT_FILE_BYTES) { throw new Error(`Support file is too large: ${filePath}`); } + if (file.content.includes("\0")) { + throw new Error(`Support files must be UTF-8 text: ${filePath}`); + } totalBytes += sizeBytes; if (totalBytes > MAX_PROPOSAL_SUPPORT_FILES_TOTAL_BYTES) { throw new Error("Skill proposal support files exceed the total size limit.");