fix(cli): guard secrets plan JSON.parse against malformed input (#109721)

* fix(cli): guard secrets plan JSON.parse against malformed input

readPlanFile reads a user-specified file and parses it with JSON.parse
without a try-catch. A malformed or corrupted plan file produces a raw
SyntaxError that is not user-actionable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: tighten malformed secrets plan proof

Co-authored-by: 丁宇婷0668001435 <ding.yuting@xydigit.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
krissding
2026-07-17 17:00:35 +08:00
committed by GitHub
parent ce4a16b0d3
commit af469c85cf
2 changed files with 22 additions and 3 deletions
+16 -2
View File
@@ -125,12 +125,15 @@ function createSecretsApplyResult(options?: {
};
}
async function withPlanFile(run: (planPath: string) => Promise<void>) {
async function withPlanFile(
run: (planPath: string) => Promise<void>,
contents = `${JSON.stringify(createManualSecretsPlan())}\n`,
) {
const planPath = path.join(
os.tmpdir(),
`openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
await fs.writeFile(planPath, `${JSON.stringify(createManualSecretsPlan())}\n`, "utf8");
await fs.writeFile(planPath, contents, "utf8");
try {
await run(planPath);
} finally {
@@ -385,6 +388,17 @@ describe("secrets CLI", () => {
});
});
it("shows a user-friendly error when the secrets plan file is malformed JSON", async () => {
await withPlanFile(async (planPath) => {
await expect(
createProgram().parseAsync(["secrets", "apply", "--from", planPath], { from: "user" }),
).rejects.toThrow("__exit__:1");
expect(runtimeErrors.at(-1)).toContain(`Malformed JSON in secrets plan file: ${planPath}`);
expect(runSecretsApply).not.toHaveBeenCalled();
}, "{invalid json");
});
it("does not print skipped-exec note when apply dry-run skippedExecRefs is zero", async () => {
await withPlanFile(async (planPath) => {
runSecretsApply.mockResolvedValue(createSecretsApplyResult({ resolvabilityComplete: false }));
+6 -1
View File
@@ -53,7 +53,12 @@ async function readPlanFile(pathname: string): Promise<SecretsApplyPlan> {
import("../secrets/plan.js"),
]);
const raw = readFileSync(pathname, "utf8");
const parsed = JSON.parse(raw) as unknown;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new Error(`Malformed JSON in secrets plan file: ${pathname}`, { cause: err });
}
if (!isSecretsApplyPlan(parsed)) {
throw new Error(
`Invalid secrets plan file: ${pathname}. Generate a fresh plan with ${formatCliCommand("openclaw secrets configure --plan-out <path>")}.`,