fix: add A2UI traversal regression coverage (openclaw#10525) thanks @abdelsfane

This commit is contained in:
Peter Steinberger
2026-02-13 15:27:38 +01:00
parent 21379efa76
commit 64547d6f90
2 changed files with 79 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/Canvas: serve A2UI assets via the shared safe-open path (`openFileWithinRoot`) to close traversal/TOCTOU gaps, with traversal and symlink regression coverage. (#10525) Thanks @abdelsfane.
- Security/Gateway + ACP: block high-risk tools (`sessions_spawn`, `sessions_send`, `gateway`, `whatsapp_login`) from HTTP `/tools/invoke` by default with `gateway.tools.{allow,deny}` overrides, and harden ACP permission selection to fail closed when tool identity/options are ambiguous while supporting `allow_always`/`reject_always`. (#15390) Thanks @aether-ai-agent.
- MS Teams: preserve parsed mention entities/text when appending OneDrive fallback file links, and accept broader real-world Teams mention ID formats (`29:...`, `8:orgid:...`) while still rejecting placeholder patterns. (#15436) Thanks @hyojin.
- Security/Audit: distinguish external webhooks (`hooks.enabled`) from internal hooks (`hooks.internal.enabled`) in attack-surface summaries to avoid false exposure signals when only internal hooks are enabled. (#13474) Thanks @mcaxtr.

View File

@@ -7,7 +7,7 @@ import { describe, expect, it, vi } from "vitest";
import { WebSocket } from "ws";
import { rawDataToString } from "../infra/ws.js";
import { defaultRuntime } from "../runtime.js";
import { CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } from "./a2ui.js";
import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } from "./a2ui.js";
import { createCanvasHostHandler, startCanvasHost } from "./server.js";
describe("canvas host", () => {
@@ -246,4 +246,81 @@ describe("canvas host", () => {
await fs.rm(dir, { recursive: true, force: true });
}
});
it("rejects traversal-style A2UI asset requests", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-"));
const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui");
const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js");
let createdBundle = false;
try {
await fs.stat(bundlePath);
} catch {
await fs.writeFile(bundlePath, "window.openclawA2UI = {};", "utf8");
createdBundle = true;
}
const server = await startCanvasHost({
runtime: defaultRuntime,
rootDir: dir,
port: 0,
listenHost: "127.0.0.1",
allowInTests: true,
});
try {
const res = await fetch(`http://127.0.0.1:${server.port}${A2UI_PATH}/%2e%2e%2fpackage.json`);
expect(res.status).toBe(404);
expect(await res.text()).toBe("not found");
} finally {
await server.close();
if (createdBundle) {
await fs.rm(bundlePath, { force: true });
}
await fs.rm(dir, { recursive: true, force: true });
}
});
it("rejects A2UI symlink escapes", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-"));
const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui");
const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js");
const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`;
const linkPath = path.join(a2uiRoot, linkName);
let createdBundle = false;
let createdLink = false;
try {
await fs.stat(bundlePath);
} catch {
await fs.writeFile(bundlePath, "window.openclawA2UI = {};", "utf8");
createdBundle = true;
}
await fs.symlink(path.join(process.cwd(), "package.json"), linkPath);
createdLink = true;
const server = await startCanvasHost({
runtime: defaultRuntime,
rootDir: dir,
port: 0,
listenHost: "127.0.0.1",
allowInTests: true,
});
try {
const res = await fetch(`http://127.0.0.1:${server.port}${A2UI_PATH}/${linkName}`);
expect(res.status).toBe(404);
expect(await res.text()).toBe("not found");
} finally {
await server.close();
if (createdLink) {
await fs.rm(linkPath, { force: true });
}
if (createdBundle) {
await fs.rm(bundlePath, { force: true });
}
await fs.rm(dir, { recursive: true, force: true });
}
});
});