QA: address check triage findings

This commit is contained in:
Gustavo Madeira Santana
2026-04-16 02:29:48 -04:00
parent e372965e3b
commit 790418b93b
8 changed files with 84 additions and 19 deletions

View File

@@ -74,4 +74,18 @@ describe("qa-lab server ui helpers", () => {
expect(tryResolveUiAsset("/", uiDistDir, rootDir)).toBe(path.join(uiDistDir, "index.html"));
expect(tryResolveUiAsset("/../dist-other/secret.txt", uiDistDir, rootDir)).toBeNull();
});
it("rejects malformed percent-encoded UI asset paths", async () => {
const uiDistDir = await mkdtemp(path.join(os.tmpdir(), "qa-lab-ui-malformed-"));
cleanups.push(async () => {
await rm(uiDistDir, { recursive: true, force: true });
});
await writeFile(
path.join(uiDistDir, "index.html"),
"<!doctype html><html><body>bundle-root</body></html>",
"utf8",
);
expect(tryResolveUiAsset("/%E0%A4", uiDistDir, uiDistDir)).toBeNull();
});
});

View File

@@ -269,7 +269,12 @@ export function tryResolveUiAsset(
return null;
}
const safePath = pathname === "/" ? "/index.html" : pathname;
const decoded = decodeURIComponent(safePath);
let decoded: string;
try {
decoded = decodeURIComponent(safePath);
} catch {
return null;
}
const candidate = path.resolve(distDir, `.${decoded.startsWith("/") ? decoded : `/${decoded}`}`);
const relative = path.relative(distDir, candidate);
if (relative.startsWith("..") || path.isAbsolute(relative)) {

View File

@@ -179,6 +179,21 @@ describe("qa suite planning helpers", () => {
});
});
it("ignores prototype-mutating keys in scenario startup config patches", () => {
const scenarios = [
makeQaSuiteTestScenario("polluted", {
gatewayConfigPatch: JSON.parse(
`{"plugins":{"entries":{}},"__proto__":{"polluted":true},"constructor":{"prototype":{"polluted":true}}}`,
) as Record<string, unknown>,
}),
];
const patch = collectQaSuiteGatewayConfigPatch(scenarios);
expect(patch).toEqual({ plugins: { entries: {} } });
expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
});
it("collects gateway runtime options across selected scenarios", () => {
const scenarios = [
makeQaSuiteTestScenario("plain"),

View File

@@ -6,6 +6,7 @@ import type { QaTransportId } from "./qa-transport-registry.js";
import { readQaBootstrapScenarioCatalog } from "./scenario-catalog.js";
const DEFAULT_QA_SUITE_CONCURRENCY = 64;
const QA_MERGE_PATCH_BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
type QaSeedScenario = ReturnType<typeof readQaBootstrapScenarioCatalog>["scenarios"][number];
@@ -108,6 +109,9 @@ function applyQaMergePatch(base: unknown, patch: unknown): unknown {
}
const result = isQaPlainObject(base) ? { ...base } : {};
for (const [key, value] of Object.entries(patch)) {
if (QA_MERGE_PATCH_BLOCKED_KEYS.has(key)) {
continue;
}
if (value === null) {
delete result[key];
continue;

View File

@@ -238,8 +238,9 @@ describe("matrix driver client", () => {
expect(requests[0]?.url).toBe(
"http://127.0.0.1:28008/_matrix/media/v3/upload?filename=red-top-blue-bottom.png",
);
expect(requests[0]?.body instanceof Uint8Array || Buffer.isBuffer(requests[0]?.body)).toBe(
true,
expect(requests[0]?.body).toBeInstanceOf(Uint8Array);
expect(Array.from(requests[0]?.body as Uint8Array)).toEqual(
Array.from(Buffer.from("png-bytes")),
);
expect(requests[1]?.url).toContain(
"/_matrix/client/v3/rooms/!room%3Amatrix-qa.test/send/m.room.message/",

View File

@@ -263,6 +263,10 @@ async function uploadMatrixQaContent(params: {
if (fileName) {
url.searchParams.set("filename", fileName);
}
const uploadBody: Uint8Array<ArrayBuffer> =
params.buffer.buffer instanceof ArrayBuffer
? new Uint8Array(params.buffer.buffer, params.buffer.byteOffset, params.buffer.byteLength)
: Uint8Array.from(params.buffer);
const response = await params.fetchImpl(url, {
method: "POST",
headers: {
@@ -270,7 +274,7 @@ async function uploadMatrixQaContent(params: {
"content-type": params.contentType ?? "application/octet-stream",
...(params.accessToken ? { authorization: `Bearer ${params.accessToken}` } : {}),
},
body: new Uint8Array(params.buffer),
body: uploadBody,
signal: AbortSignal.timeout(20_000),
});
const body = (await response.json().catch(() => ({}))) as {

View File

@@ -159,6 +159,27 @@ describe("matrix observed event normalization", () => {
);
});
it("treats filename-like Matrix media bodies as attachment filenames", () => {
expect(
normalizeMatrixQaObservedEvent("!room:matrix-qa.test", {
event_id: "$image",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
content: {
body: "qa-lighthouse.png",
msgtype: "m.image",
},
}),
).toEqual(
expect.objectContaining({
attachment: {
kind: "image",
filename: "qa-lighthouse.png",
},
}),
);
});
it("normalizes membership events with explicit membership kind", () => {
expect(
normalizeMatrixQaObservedEvent("!room:matrix-qa.test", {

View File

@@ -104,6 +104,10 @@ function resolveMatrixQaAttachmentKind(msgtype: string | undefined) {
}
}
function isLikelyMatrixQaFilenameBody(value: string) {
return !value.includes("\n") && /\.[a-z0-9][a-z0-9._-]{0,24}$/i.test(value);
}
function resolveMatrixQaAttachmentSummary(params: {
body?: string;
filename?: string;
@@ -114,10 +118,14 @@ function resolveMatrixQaAttachmentSummary(params: {
return undefined;
}
const body = params.body?.trim() ?? "";
const filename = params.filename?.trim() ?? "";
const explicitFilename = params.filename?.trim() ?? "";
const inferredFilename =
!explicitFilename && body && isLikelyMatrixQaFilenameBody(body) ? body : "";
const filename = explicitFilename || inferredFilename;
const caption = body && body !== filename ? body : "";
return {
kind,
...(body && body !== filename ? { caption: body } : {}),
...(caption ? { caption } : {}),
...(filename ? { filename } : {}),
};
}
@@ -164,6 +172,11 @@ export function normalizeMatrixQaObservedEvent(
type === "m.reaction" && typeof relatesTo?.event_id === "string"
? relatesTo.event_id
: undefined;
const attachment = resolveMatrixQaAttachmentSummary({
body: typeof messageContent.body === "string" ? messageContent.body : undefined,
filename: normalizedFilename,
msgtype: normalizedMsgtype,
});
return {
kind: resolveMatrixQaObservedEventKind({ msgtype: normalizedMsgtype, type }),
@@ -208,18 +221,6 @@ export function normalizeMatrixQaObservedEvent(
},
}
: {}),
...(resolveMatrixQaAttachmentSummary({
body: typeof messageContent.body === "string" ? messageContent.body : undefined,
filename: normalizedFilename,
msgtype: normalizedMsgtype,
})
? {
attachment: resolveMatrixQaAttachmentSummary({
body: typeof messageContent.body === "string" ? messageContent.body : undefined,
filename: normalizedFilename,
msgtype: normalizedMsgtype,
}),
}
: {}),
...(attachment ? { attachment } : {}),
};
}