mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 12:41:39 +00:00
fix(plugin): align dev host asset limit with runtime
This commit is contained in:
@@ -69,6 +69,7 @@ Field names come from the plugin manifest, not the runtime. Imported records get
|
||||
- Host API 1.0 subset: `ready`, `contributionId` (readable after `ready`), `context`, `locale`, `theme`, `request`, `invoke`, `notify`, `onInit`, `onContext`, `onEvent`, `onBinary`, `sendBinary`, resource reads and `openWorkbench`.
|
||||
- Backend transports: default `stdio-jsonl` and explicit `stdio-framed`, protocol version 1. Initialization verifies plugin identity and version. Binary channels require framed transport.
|
||||
- Permissions: event, binary and workbench navigation permissions are enforced. Unimplemented methods, such as `host.openFilesystem`, return errors. Native connection actions, query-result contributions and the DBX component kit are not emulated.
|
||||
- UI entrypoints and individual asset files: 32 MiB, matching the installed DBX host. Larger files and directories are rejected; path and symlink isolation remains enforced.
|
||||
- JSON bridge parameters: 2 MiB. UI binary messages: 8 MiB. Sidecar JSON: 8 MiB. Sidecar binary: 64 MiB. Explicit bridge timeouts are clamped to 1–120000 ms, matching the host baseline.
|
||||
- UI resources must be inline or loaded through `readAssetUrl`; relative module URLs are not a replacement for the declared asset bridge. The iframe uses `sandbox="allow-scripts"`, restrictive CSP and pure-data context snapshots. Direct networking is not enabled by the development runtime.
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { readFile, realpath, stat } from "node:fs/promises";
|
||||
import { resolve, relative, isAbsolute, extname } from "node:path";
|
||||
// Keep UI entrypoints and assets within the same bound as the installed host's
|
||||
// MAX_PLUGIN_UI_ASSET_BYTES (crates/dbx-plugin-runtime/src/plugins/assets.rs).
|
||||
const MAX_PLUGIN_UI_ASSET_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
export async function readAsset(root, path) {
|
||||
if (typeof path !== "string" || !path || path.includes("\0") || isAbsolute(path)) throw new Error("Invalid asset path");
|
||||
const base = await realpath(root),
|
||||
@@ -7,7 +11,8 @@ export async function readAsset(root, path) {
|
||||
const rel = relative(base, file);
|
||||
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("Asset is outside UI root");
|
||||
const info = await stat(file);
|
||||
if (!info.isFile() || info.size > 8 * 1024 * 1024) throw new Error("Asset exceeds limit or is not a file");
|
||||
if (!info.isFile()) throw new Error("Asset is not a file");
|
||||
if (info.size > MAX_PLUGIN_UI_ASSET_BYTES) throw new Error(`Asset exceeds ${MAX_PLUGIN_UI_ASSET_BYTES} byte limit`);
|
||||
const types = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", ".woff2": "font/woff2" };
|
||||
return { dataBase64: (await readFile(file)).toString("base64"), contentType: types[extname(file)] || "application/octet-stream" };
|
||||
}
|
||||
|
||||
@@ -168,10 +168,10 @@ test("refresh cleanup does not disconnect a connection still used by another pag
|
||||
assert.equal(closed.value.connections[0].connected, false);
|
||||
});
|
||||
async function fixture(t, frontend = false, overrides = {}) {
|
||||
const { pluginManifest = manifest, ...hostOverrides } = overrides;
|
||||
const { pluginManifest = manifest, uiHtml = "<html><head></head><body>Test</body></html>", ...hostOverrides } = overrides;
|
||||
const root = await mkdtemp(join(tmpdir(), "dbx-mock-test-"));
|
||||
await mkdir(join(root, "ui"));
|
||||
await writeFile(join(root, "ui/index.html"), "<html><head></head><body>Test</body></html>");
|
||||
await writeFile(join(root, "ui/index.html"), uiHtml);
|
||||
const actual = frontend ? { ...pluginManifest, entrypoints: { ui: pluginManifest.entrypoints.ui }, contributions: pluginManifest.contributions.filter((c) => c.type === "workbench") } : pluginManifest;
|
||||
await writeFile(join(root, "manifest.json"), JSON.stringify(actual));
|
||||
const host = await createMockHost({
|
||||
@@ -439,6 +439,32 @@ test("loopback endpoint rejects cross-origin, forged Host, CSRF and unowned fram
|
||||
const boot = await other.json();
|
||||
assert.equal((await request("frame-document", { frameId: f.id }, { ...headers, Cookie: other.headers.get("set-cookie").split(";")[0], "X-Mock-Csrf": boot.csrf })).status, 400);
|
||||
});
|
||||
test("development host opens a self-contained UI larger than 8 MiB (#11286)", async (t) => {
|
||||
const payload = "x".repeat(12 * 1024 * 1024);
|
||||
const uiHtml = `<html><head></head><body><script>/*${payload}*/</script><p>Complete UI</p></body></html>`;
|
||||
const { request } = await fixture(t, true, { uiHtml });
|
||||
const opened = await request("workbenches/open", { contributionId: "example.main" });
|
||||
assert.equal(opened.status, 200);
|
||||
const document = await request("frame-document", { frameId: opened.value.frame.id });
|
||||
assert.equal(document.status, 200);
|
||||
assert.ok(document.value.html.includes(payload), "the UI payload must not be truncated");
|
||||
assert.ok(document.value.html.includes("<p>Complete UI</p>"));
|
||||
});
|
||||
|
||||
test("asset reader accepts 32 MiB and rejects larger files and directories", async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "dbx-asset-size-"));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const bytes = Buffer.alloc(32 * 1024 * 1024, 0x61);
|
||||
await writeFile(join(root, "at-limit.bin"), bytes);
|
||||
const asset = await readAsset(root, "at-limit.bin");
|
||||
assert.equal(asset.contentType, "application/octet-stream");
|
||||
assert.ok(Buffer.from(asset.dataBase64, "base64").equals(bytes));
|
||||
await writeFile(join(root, "too-large.bin"), Buffer.alloc(bytes.length + 1));
|
||||
await assert.rejects(readAsset(root, "too-large.bin"), /Asset exceeds 33554432 byte limit/);
|
||||
await mkdir(join(root, "directory"));
|
||||
await assert.rejects(readAsset(root, "directory"), /Asset is not a file/);
|
||||
});
|
||||
|
||||
test("asset reader rejects traversal and symlinks outside the UI root", async (t) => {
|
||||
const { root } = await fixture(t);
|
||||
await writeFile(join(root, "secret"), "test-secret");
|
||||
@@ -499,7 +525,7 @@ test("icon reads reject unsafe paths, missing files, non-images, symlink escapes
|
||||
await mkdir(join(root, "assets"));
|
||||
await writeFile(join(outside, "private.svg"), "private-icon-content");
|
||||
await symlink(join(outside, "private.svg"), join(root, "assets/link.svg"));
|
||||
await writeFile(join(root, "assets/large.svg"), Buffer.alloc(8 * 1024 * 1024 + 1));
|
||||
await writeFile(join(root, "assets/large.svg"), Buffer.alloc(32 * 1024 * 1024 + 1));
|
||||
for (const contribution of pluginManifest.contributions) {
|
||||
const result = await request("icon", { contributionId: contribution.id });
|
||||
assert.equal(result.status, 400, contribution.icon);
|
||||
|
||||
Reference in New Issue
Block a user