60 lines
3.0 KiB
TypeScript
60 lines
3.0 KiB
TypeScript
import { expect } from "vitest";
|
|
import { test } from "@openwork/testkit";
|
|
|
|
import { safeAttachmentFilename } from "../../apps/app/src/react-app/domains/session/sync/attachment-file-part";
|
|
import { normalizeWorkspaceRelativePath } from "../../apps/server/src/routes/files";
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
test("upload filename and path boundaries reject unsafe writes", ({ evidence }) => {
|
|
const asciiInput = `${"a".repeat(400)}.pdf`;
|
|
const multibyteInput = `${"李".repeat(200)}.txt`;
|
|
const asciiName = safeAttachmentFilename(asciiInput);
|
|
const multibyteName = safeAttachmentFilename(multibyteInput);
|
|
|
|
expect(encoder.encode(asciiInput).byteLength).toBeGreaterThan(255);
|
|
expect(encoder.encode(multibyteInput).byteLength).toBeGreaterThan(255);
|
|
expect(encoder.encode(asciiName).byteLength).toBeLessThanOrEqual(255);
|
|
expect(encoder.encode(multibyteName).byteLength).toBeLessThanOrEqual(255);
|
|
expect(asciiName.endsWith(".pdf")).toBe(true);
|
|
expect(multibyteName.endsWith(".txt")).toBe(true);
|
|
expect(asciiName).not.toBe(asciiInput);
|
|
expect(multibyteName).not.toBe(multibyteInput);
|
|
expect(multibyteName).not.toContain("�");
|
|
|
|
evidence.recordAssertionEvidence(
|
|
"Oversized ASCII and multibyte upload names are bounded with extensions preserved",
|
|
`The ${encoder.encode(asciiInput).byteLength}-byte ASCII and ${encoder.encode(multibyteInput).byteLength}-byte multibyte inputs became ${encoder.encode(asciiName).byteLength}-byte .pdf and ${encoder.encode(multibyteName).byteLength}-byte .txt names without a broken UTF-8 character.`,
|
|
true,
|
|
);
|
|
const unsafePaths = [
|
|
["reports/CON.txt", "Windows reserved device names"],
|
|
["reports/screenshot.png:metadata", "must not contain colons"],
|
|
["reports/screenshot.png.", "must not end with a dot or space"],
|
|
["reports/screenshot.png ", "must not end with a dot or space"],
|
|
] as const;
|
|
|
|
for (const [path, message] of unsafePaths) {
|
|
expect(() => normalizeWorkspaceRelativePath(path, { allowSubdirs: true })).toThrow(message);
|
|
}
|
|
|
|
for (const segment of ["a".repeat(256), "李".repeat(86)]) {
|
|
expect(encoder.encode(segment).byteLength).toBeGreaterThan(255);
|
|
expect(() => normalizeWorkspaceRelativePath(`reports/${segment}`, { allowSubdirs: true }))
|
|
.toThrow("Path components must not exceed 255 UTF-8 bytes");
|
|
}
|
|
|
|
const boundaryName = `${"a".repeat(251)}.txt`;
|
|
expect(encoder.encode(boundaryName).byteLength).toBe(255);
|
|
expect(normalizeWorkspaceRelativePath(`reports/${boundaryName}`, { allowSubdirs: true }))
|
|
.toBe(`reports/${boundaryName}`);
|
|
expect(normalizeWorkspaceRelativePath("reports/console.txt", { allowSubdirs: true }))
|
|
.toBe("reports/console.txt");
|
|
|
|
evidence.recordAssertionEvidence(
|
|
"Windows-unsafe and oversized upload path components are rejected at the server boundary",
|
|
"Reserved-device, alternate-data-stream, trailing-dot, trailing-space, 256-byte ASCII, and 258-byte multibyte components all threw their expected invalid-path messages; an exact 255-byte filename and an ordinary lookalike remained valid.",
|
|
true,
|
|
);
|
|
});
|