Skip to content

Commit 87ec6d5

Browse files
Guard Runtime.enable sender lookup in a11y context collection
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent fe06d79 commit 87ec6d5

3 files changed

Lines changed: 112 additions & 8 deletions

File tree

currentState.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
198198
- Hardened a11y runtime-context collection listener wiring:
199199
- `collectExecutionContexts()` now guards `session.on`/`session.off` method reads and listener attach/detach calls,
200200
- context collection now tolerates trap-prone runtime listener method getters while preserving sanitized diagnostics in debug mode.
201+
- `collectExecutionContexts()` now also guards trap-prone `session.send` reads for `Runtime.enable`, preventing getter traps from aborting the context collection path.
201202
- Hardened CDP command dispatch in Playwright session adapter:
202203
- `PlaywrightSessionAdapter.send()` now guards trap-prone `session.send` method reads and wraps sync send failures with sanitized/diagnostic context.
203204
- Hardened Playwright session listener wrappers:

src/context-providers/a11y-dom/index.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,75 @@ describe("getA11yDOM error formatting", () => {
329329
errorSpy.mockRestore();
330330
}
331331
});
332+
333+
it("continues when runtime sender getter traps during context collection", async () => {
334+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
335+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
336+
const page = {
337+
evaluate: jest.fn().mockResolvedValue(undefined),
338+
url: jest.fn(() => "https://example.com"),
339+
} as unknown as Page;
340+
let sendGetterReadCount = 0;
341+
const session = {
342+
id: "session-1",
343+
get send() {
344+
sendGetterReadCount += 1;
345+
if (sendGetterReadCount === 2) {
346+
throw new Error(`runtime-send\u0000\n${"x".repeat(2_000)}`);
347+
}
348+
return async (method: string) => {
349+
if (method === "Accessibility.getFullAXTree") {
350+
throw new Error("stop after context collection");
351+
}
352+
return {};
353+
};
354+
},
355+
on: jest.fn(),
356+
off: jest.fn(),
357+
};
358+
getCDPClientMock.mockResolvedValue({
359+
acquireSession: jest.fn().mockResolvedValue(session),
360+
});
361+
getOrCreateFrameContextManagerMock.mockReturnValue({
362+
setDebug: jest.fn(),
363+
ensureInitialized: jest.fn().mockResolvedValue(undefined),
364+
captureOOPIFs: jest.fn().mockResolvedValue(undefined),
365+
setFrameFilteringEnabled: jest.fn(),
366+
});
367+
buildBackendIdMapsMock.mockResolvedValue({
368+
frameMap: new Map([
369+
[
370+
1,
371+
{
372+
frameIndex: 1,
373+
siblingPosition: 0,
374+
src: "https://example.com/frame",
375+
xpath: "//iframe[1]",
376+
parentFrameIndex: 0,
377+
frameId: "frame-1",
378+
},
379+
],
380+
]),
381+
backendNodeMap: {},
382+
xpathMap: {},
383+
frameMetadataMap: new Map(),
384+
frameTree: new Map(),
385+
});
386+
387+
try {
388+
const result = await getA11yDOM(page, true);
389+
expect(result.domState).toBe("Error: Could not extract accessibility tree");
390+
const warning = String(
391+
warnSpy.mock.calls.find((call) =>
392+
String(call[0] ?? "").includes("Failed to read Runtime.enable sender")
393+
)?.[0] ?? ""
394+
);
395+
expect(warning).toContain("[truncated");
396+
expect(warning).not.toContain("\u0000");
397+
expect(warning).not.toContain("\n");
398+
} finally {
399+
warnSpy.mockRestore();
400+
errorSpy.mockRestore();
401+
}
402+
});
332403
});

src/context-providers/a11y-dom/index.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,29 +192,61 @@ async function collectExecutionContexts(
192192
}
193193
};
194194

195-
const onMethod = readSessionMethod("on");
196-
if (onMethod) {
195+
const enableRuntimeDomain = async (): Promise<void> => {
196+
let sendMethod: unknown;
197197
try {
198-
onMethod.call(session, "Runtime.executionContextCreated", handler);
198+
sendMethod = (session as CDPSession & { send?: unknown }).send;
199199
} catch (error) {
200200
if (debug) {
201201
console.warn(
202-
`[A11y] Failed to attach Runtime.executionContextCreated listener: ${formatA11yDiagnostic(
202+
`[A11y] Failed to read Runtime.enable sender: ${formatA11yDiagnostic(
203203
error
204204
)}`
205205
);
206206
}
207+
return;
207208
}
208-
}
209-
try {
210-
await session.send("Runtime.enable").catch((error) => {
209+
if (typeof sendMethod !== "function") {
210+
if (debug) {
211+
console.warn(
212+
"[A11y] Runtime.enable sender unavailable during context collection"
213+
);
214+
}
215+
return;
216+
}
217+
try {
218+
await (
219+
sendMethod as (
220+
this: CDPSession,
221+
method: "Runtime.enable"
222+
) => Promise<unknown>
223+
).call(session, "Runtime.enable");
224+
} catch (error) {
211225
if (debug) {
212226
console.warn(
213227
"[A11y] Failed to enable Runtime domain for context collection. " +
214228
`Execution contexts may be missing for iframe elements. ${formatA11yDiagnostic(error)}`
215229
);
216230
}
217-
});
231+
}
232+
};
233+
234+
const onMethod = readSessionMethod("on");
235+
if (onMethod) {
236+
try {
237+
onMethod.call(session, "Runtime.executionContextCreated", handler);
238+
} catch (error) {
239+
if (debug) {
240+
console.warn(
241+
`[A11y] Failed to attach Runtime.executionContextCreated listener: ${formatA11yDiagnostic(
242+
error
243+
)}`
244+
);
245+
}
246+
}
247+
}
248+
try {
249+
await enableRuntimeDomain();
218250
await waitPromise;
219251
} finally {
220252
const offMethod = readSessionMethod("off");

0 commit comments

Comments
 (0)