From 790418b93b81f1297a3407f08a756cd0118f01b4 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Thu, 16 Apr 2026 02:29:48 -0400 Subject: [PATCH] QA: address check triage findings --- extensions/qa-lab/src/lab-server-ui.test.ts | 14 +++++++++ extensions/qa-lab/src/lab-server-ui.ts | 7 ++++- extensions/qa-lab/src/suite-planning.test.ts | 15 +++++++++ extensions/qa-lab/src/suite-planning.ts | 4 +++ .../qa-matrix/src/substrate/client.test.ts | 5 +-- extensions/qa-matrix/src/substrate/client.ts | 6 +++- .../qa-matrix/src/substrate/events.test.ts | 21 +++++++++++++ extensions/qa-matrix/src/substrate/events.ts | 31 ++++++++++--------- 8 files changed, 84 insertions(+), 19 deletions(-) diff --git a/extensions/qa-lab/src/lab-server-ui.test.ts b/extensions/qa-lab/src/lab-server-ui.test.ts index 1d5865fa946..47748e6d261 100644 --- a/extensions/qa-lab/src/lab-server-ui.test.ts +++ b/extensions/qa-lab/src/lab-server-ui.test.ts @@ -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"), + "bundle-root", + "utf8", + ); + + expect(tryResolveUiAsset("/%E0%A4", uiDistDir, uiDistDir)).toBeNull(); + }); }); diff --git a/extensions/qa-lab/src/lab-server-ui.ts b/extensions/qa-lab/src/lab-server-ui.ts index 7a4509c6591..53c9d110597 100644 --- a/extensions/qa-lab/src/lab-server-ui.ts +++ b/extensions/qa-lab/src/lab-server-ui.ts @@ -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)) { diff --git a/extensions/qa-lab/src/suite-planning.test.ts b/extensions/qa-lab/src/suite-planning.test.ts index f7c205ef2f7..15ca4a1e7a6 100644 --- a/extensions/qa-lab/src/suite-planning.test.ts +++ b/extensions/qa-lab/src/suite-planning.test.ts @@ -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, + }), + ]; + + 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"), diff --git a/extensions/qa-lab/src/suite-planning.ts b/extensions/qa-lab/src/suite-planning.ts index 2433593c23d..1872a96c492 100644 --- a/extensions/qa-lab/src/suite-planning.ts +++ b/extensions/qa-lab/src/suite-planning.ts @@ -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["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; diff --git a/extensions/qa-matrix/src/substrate/client.test.ts b/extensions/qa-matrix/src/substrate/client.test.ts index 142c4caf364..55d49de2eab 100644 --- a/extensions/qa-matrix/src/substrate/client.test.ts +++ b/extensions/qa-matrix/src/substrate/client.test.ts @@ -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/", diff --git a/extensions/qa-matrix/src/substrate/client.ts b/extensions/qa-matrix/src/substrate/client.ts index 54f1b8c9ad2..92f30aa27d4 100644 --- a/extensions/qa-matrix/src/substrate/client.ts +++ b/extensions/qa-matrix/src/substrate/client.ts @@ -263,6 +263,10 @@ async function uploadMatrixQaContent(params: { if (fileName) { url.searchParams.set("filename", fileName); } + const uploadBody: Uint8Array = + 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 { diff --git a/extensions/qa-matrix/src/substrate/events.test.ts b/extensions/qa-matrix/src/substrate/events.test.ts index eddd952ff0c..fcbb2513f36 100644 --- a/extensions/qa-matrix/src/substrate/events.test.ts +++ b/extensions/qa-matrix/src/substrate/events.test.ts @@ -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", { diff --git a/extensions/qa-matrix/src/substrate/events.ts b/extensions/qa-matrix/src/substrate/events.ts index 83b5334c6fe..b15a53c90a9 100644 --- a/extensions/qa-matrix/src/substrate/events.ts +++ b/extensions/qa-matrix/src/substrate/events.ts @@ -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 } : {}), }; }