mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix: reject non-text skill proposal files
This commit is contained in:
@@ -541,7 +541,7 @@ export function registerSkillsCli(program: Command) {
|
||||
.option("--proposal <path>", "Path to PROPOSAL.md draft content")
|
||||
.option(
|
||||
"--proposal-dir <path>",
|
||||
"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 <text>", "Research or improvement goal")
|
||||
.option("--evidence <text>", "Evidence or notes for the proposal")
|
||||
@@ -592,7 +592,7 @@ export function registerSkillsCli(program: Command) {
|
||||
.option("--proposal <path>", "Path to PROPOSAL.md draft content")
|
||||
.option(
|
||||
"--proposal-dir <path>",
|
||||
"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 <text>", "Research or improvement goal")
|
||||
.option("--evidence <text>", "Evidence or notes for the proposal")
|
||||
@@ -646,7 +646,7 @@ export function registerSkillsCli(program: Command) {
|
||||
.option("--proposal <path>", "Path to revised PROPOSAL.md draft content")
|
||||
.option(
|
||||
"--proposal-dir <path>",
|
||||
"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 <description>", "Replacement proposal description")
|
||||
.option("--goal <text>", "Replacement research or improvement goal")
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<stri
|
||||
filePath,
|
||||
maxBytes: MAX_PROPOSAL_DRAFT_BYTES,
|
||||
});
|
||||
return read.buffer.toString("utf8");
|
||||
return decodeProposalTextFile(read.buffer, filePath);
|
||||
}
|
||||
|
||||
export async function readSkillProposalDraftDirectory(dirPath: string): Promise<{
|
||||
@@ -129,19 +130,34 @@ export async function readSkillProposalDraftDirectory(dirPath: string): Promise<
|
||||
throw new Error(`Proposal support file must be a regular file: ${relativePath}`);
|
||||
}
|
||||
const supportPath = normalizeSkillProposalSupportPath(relativePath);
|
||||
const stats = await fs.stat(entry.path);
|
||||
if ((stats.mode & 0o111) !== 0) {
|
||||
throw new Error(`Proposal support files must not be executable: ${relativePath}`);
|
||||
}
|
||||
const read = await draftRoot.read(relativePath, {
|
||||
hardlinks: "reject",
|
||||
maxBytes: MAX_PROPOSAL_SUPPORT_FILE_BYTES,
|
||||
symlinks: "reject",
|
||||
});
|
||||
supportFiles.push({ path: supportPath, content: read.buffer.toString("utf8") });
|
||||
supportFiles.push({
|
||||
path: supportPath,
|
||||
content: decodeProposalTextFile(read.buffer, relativePath),
|
||||
});
|
||||
}
|
||||
return {
|
||||
content: proposal.buffer.toString("utf8"),
|
||||
content: decodeProposalTextFile(proposal.buffer, "PROPOSAL.md"),
|
||||
supportFiles,
|
||||
};
|
||||
}
|
||||
|
||||
function decodeProposalTextFile(buffer: Buffer, label: string): string {
|
||||
const content = buffer.toString("utf8");
|
||||
if (!Buffer.from(content, "utf8").equals(buffer) || content.includes("\0")) {
|
||||
throw new Error(`Proposal files must be UTF-8 text: ${label}`);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function inspectSkillProposal(
|
||||
proposalId: string,
|
||||
options: SkillProposalScopeOptions = {},
|
||||
|
||||
@@ -169,6 +169,9 @@ export function prepareSkillProposalSupportFiles(
|
||||
if (sizeBytes > 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.");
|
||||
|
||||
Reference in New Issue
Block a user