fix(security): bound policy finding line numbers

This commit is contained in:
jesse-merhi
2026-08-11 17:17:16 +10:00
parent 84a8d4d571
commit b05da5f26c
3 changed files with 28 additions and 2 deletions
+2 -1
View File
@@ -180,7 +180,8 @@ write one JSON object on stdout with an `allow`, `warn`, or `block` decision.
`findings` array. Each finding requires non-empty string `ruleId` and `message`
fields plus a `severity` of `info`, `warn`, or `critical`. Optional `file` and
`evidence` values must be non-empty strings; a finite numeric `line` is rounded
down and clamped to at least 1. Malformed finding entries are ignored, and
down and clamped to the safe-integer range from 1 through `Number.MAX_SAFE_INTEGER`.
Malformed finding entries are ignored, and
invalid optional fields are omitted. A non-array `findings` value is treated as
absent. Operator-facing reason and finding text are limited to 1,000 characters.
OpenClaw retains at most 100 normalized findings for display. Only a `warn`
+1 -1
View File
@@ -52,7 +52,7 @@ const installPolicyFindingSchema = z
line: z
.number()
.finite()
.transform((value) => Math.max(1, Math.floor(value)))
.transform((value) => Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, Math.floor(value))))
.optional()
.catch(undefined),
evidence: optionalFindingTextSchema,
+25
View File
@@ -451,6 +451,31 @@ describe("runInstallPolicy", () => {
expect(debugLogs.filter((message) => message.endsWith(": warned"))).toHaveLength(1);
});
it("normalizes warning finding lines to positive safe integers", async () => {
const result = await runInstallPolicy({
config: configWithPolicy(scriptPath, {
POLICY_RESPONSE: JSON.stringify({
protocolVersion: 1,
decision: "warn",
reason: "review line normalization",
findings: [-2, 12.9, 1e100].map((line, index) => ({
ruleId: `line-${String(index)}`,
severity: "warn",
message: "Review line",
line,
})),
}),
}),
request: baseRequest(sourceDir),
});
expect(result?.findings?.map((finding) => finding.line)).toEqual([
1,
12,
Number.MAX_SAFE_INTEGER,
]);
});
it("fingerprints warning reason changes beyond the display limit", async () => {
const sharedPrefix = "r".repeat(1000);
const runWarning = async (reason: string) =>