fix(canvas): reject malformed document paths

This commit is contained in:
Vincent Koc
2026-05-14 13:22:59 +08:00
parent bb8aa0cfe2
commit b73d01f13b
3 changed files with 34 additions and 11 deletions

View File

@@ -13,7 +13,7 @@ Docs: https://docs.openclaw.ai
- CLI tables: preserve muted/color styling on wrapped continuation lines after multiline cells, keeping `openclaw plugins list` descriptions readable.
- iOS: restore first-use Contacts, Calendar, and Reminders permission prompts and add Privacy & Access status/actions in Settings. Thanks @BunsDev.
- Canvas: return not found for malformed percent-encoded Canvas/A2UI asset paths and keep decoded parent traversal blocked before path normalization.
- Canvas: return not found for malformed percent-encoded Canvas/A2UI/document asset paths and keep decoded parent traversal blocked before path normalization.
- Agents: allow dot-dot-prefixed filenames such as `..note.txt` through sandbox FS bridge, remote sandbox reads, and apply_patch summaries without mistaking the name for parent traversal.
- CLI/migrate: humanize Codex conflict-status messaging across the migrate UI so selection prompts and plan/result rows say "Codex skill already installed in workspace" instead of surfacing internal `MIGRATION_REASON_*` codes. Thanks @sjf.
- CLI/migrate: render migrate result rows with distinct glyphs for manual-review (🔍) and archive (📖) items instead of the misleading "skipped" and "migrated" checkmarks, so users can see which entries still need attention versus which were filed away. Thanks @sjf.

View File

@@ -239,4 +239,26 @@ describe("canvas documents", () => {
),
).toBeNull();
});
it("rejects malformed encoded hosted canvas document paths", async () => {
const stateDir = await mkdtemp(path.join(tmpdir(), "openclaw-canvas-documents-"));
tempDirs.push(stateDir);
const documentId = "cv_malformed";
const documentDir = resolveCanvasDocumentDir(documentId, { stateDir });
await mkdir(documentDir, { recursive: true });
await writeFile(path.join(documentDir, "%E0%A4%A.html"), "literal-percent-name", "utf8");
expect(
resolveCanvasHttpPathToLocalPath(
`/__openclaw__/canvas/documents/${documentId}/%E0%A4%A.html`,
{ stateDir },
),
).toBeNull();
expect(
resolveCanvasHttpPathToLocalPath(
`/__openclaw__/canvas/documents/${documentId}/%25E0%25A4%25A.html`,
{ stateDir },
),
).toBe(path.join(documentDir, "%E0%A4%A.html"));
});
});

View File

@@ -153,16 +153,17 @@ export function resolveCanvasHttpPathToLocalPath(
}
const pathWithoutQuery = trimmed.replace(/[?#].*$/, "");
const relative = pathWithoutQuery.slice(prefix.length);
const segments = relative
.split("/")
.map((segment) => {
try {
return decodeURIComponent(segment);
} catch {
return segment;
}
})
.filter(Boolean);
const segments: string[] = [];
for (const segment of relative.split("/")) {
if (!segment) {
continue;
}
try {
segments.push(decodeURIComponent(segment));
} catch {
return null;
}
}
if (segments.length < 2) {
return null;
}