68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
import { mkdtemp, mkdir, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { expect, test } from "@playwright/test";
|
|
import {
|
|
readTrustedLocalImage,
|
|
resolveTrustedLocalImageCandidate,
|
|
} from "../../lib/video-canvas/local-image-input";
|
|
|
|
test.describe("video canvas element mark local image input", () => {
|
|
test("maps only loopback local-upload URLs into the public upload directory", () => {
|
|
const workspaceRoot = "/workspace/project";
|
|
expect(
|
|
resolveTrustedLocalImageCandidate(
|
|
"http://127.0.0.1:3003/uploads/local/example.png?cache=1",
|
|
workspaceRoot,
|
|
),
|
|
).toEqual({
|
|
candidatePath: resolve(workspaceRoot, "public/uploads/local/example.png"),
|
|
uploadRoot: resolve(workspaceRoot, "public/uploads/local"),
|
|
});
|
|
expect(
|
|
resolveTrustedLocalImageCandidate("/uploads/local/example.png", workspaceRoot),
|
|
).toEqual({
|
|
candidatePath: resolve(workspaceRoot, "public/uploads/local/example.png"),
|
|
uploadRoot: resolve(workspaceRoot, "public/uploads/local"),
|
|
});
|
|
expect(
|
|
resolveTrustedLocalImageCandidate(
|
|
"https://assets.example.com/uploads/local/example.png",
|
|
workspaceRoot,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
test("rejects traversal and non-upload loopback paths", () => {
|
|
const workspaceRoot = "/workspace/project";
|
|
expect(
|
|
resolveTrustedLocalImageCandidate(
|
|
"http://localhost:3003/uploads/local/../../.env",
|
|
workspaceRoot,
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
resolveTrustedLocalImageCandidate(
|
|
"http://localhost:3003/api/private-image",
|
|
workspaceRoot,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
test("reads a real upload and refuses symlinks escaping the trusted directory", async () => {
|
|
const workspaceRoot = await mkdtemp(join(tmpdir(), "video-mark-input-"));
|
|
const uploadRoot = join(workspaceRoot, "public/uploads/local");
|
|
await mkdir(uploadRoot, { recursive: true });
|
|
await writeFile(join(uploadRoot, "sample.png"), Buffer.from("trusted-image"));
|
|
await writeFile(join(workspaceRoot, "secret.txt"), Buffer.from("secret"));
|
|
await symlink(join(workspaceRoot, "secret.txt"), join(uploadRoot, "escape.png"));
|
|
|
|
await expect(
|
|
readTrustedLocalImage("http://127.0.0.1:3003/uploads/local/sample.png", workspaceRoot),
|
|
).resolves.toEqual(Buffer.from("trusted-image"));
|
|
await expect(
|
|
readTrustedLocalImage("http://127.0.0.1:3003/uploads/local/escape.png", workspaceRoot),
|
|
).rejects.toThrow("本地图片路径不允许访问");
|
|
});
|
|
});
|