fix(browser): apply three-phase interaction navigation guard to pressKey and type(submit) [AI-assisted] (#63889)

* fix: address issue

* chore(changelog): add pressKey/type SSRF guard entry

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
Michael Appel
2026-04-10 13:27:53 -04:00
committed by GitHub
parent 9f97ad857a
commit e0b8ddc1a5
3 changed files with 124 additions and 25 deletions

View File

@@ -398,6 +398,115 @@ describe("pw-tools-core interaction navigation guard", () => {
});
});
it("runs the post-keypress navigation guard when navigation starts shortly after the keypress resolves", async () => {
vi.useFakeTimers();
try {
const listeners = new Set<() => void>();
let currentUrl = "http://127.0.0.1:9222/json/version";
const page = {
keyboard: {
press: vi.fn(async () => {
setTimeout(() => {
currentUrl = "http://127.0.0.1:9222/private-target";
for (const listener of listeners) {
listener();
}
}, 10);
}),
},
on: vi.fn((event: string, listener: () => void) => {
if (event === "framenavigated") {
listeners.add(listener);
}
}),
off: vi.fn((event: string, listener: () => void) => {
if (event === "framenavigated") {
listeners.delete(listener);
}
}),
url: vi.fn(() => currentUrl),
};
setPwToolsCoreCurrentPage(page);
const task = mod.pressKeyViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
key: "Enter",
ssrfPolicy: { allowPrivateNetwork: false },
});
await vi.advanceTimersByTimeAsync(10);
await task;
expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith(
{
cdpUrl: "http://127.0.0.1:18792",
page,
response: null,
ssrfPolicy: { allowPrivateNetwork: false },
targetId: "T1",
},
);
} finally {
vi.useRealTimers();
}
});
it("propagates blocked delayed submit navigation instead of reporting type success", async () => {
vi.useFakeTimers();
try {
const listeners = new Set<() => void>();
let currentUrl = "https://example.com/form";
const locator = {
fill: vi.fn(async () => {}),
press: vi.fn(async () => {
setTimeout(() => {
currentUrl = "http://127.0.0.1:9222/private-target";
for (const listener of listeners) {
listener();
}
}, 10);
}),
};
const page = {
on: vi.fn((event: string, listener: () => void) => {
if (event === "framenavigated") {
listeners.add(listener);
}
}),
off: vi.fn((event: string, listener: () => void) => {
if (event === "framenavigated") {
listeners.delete(listener);
}
}),
url: vi.fn(() => currentUrl),
};
setPwToolsCoreCurrentRefLocator(locator);
setPwToolsCoreCurrentPage(page);
const blocked = new Error("blocked delayed interaction navigation");
getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely.mockRejectedValueOnce(
blocked,
);
const task = mod.typeViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
ref: "1",
text: "hello",
submit: true,
ssrfPolicy: { allowPrivateNetwork: false },
});
const rejection = expect(task).rejects.toThrow("blocked delayed interaction navigation");
await vi.advanceTimersByTimeAsync(10);
await rejection;
expect(listeners.size).toBe(0);
} finally {
vi.useRealTimers();
}
});
it("does not run the post-click navigation guard when the url is unchanged", async () => {
const click = vi.fn(async () => {});
const page = { url: vi.fn(() => "http://127.0.0.1:9222/json/version") };

View File

@@ -379,25 +379,6 @@ function createAbortPromiseWithListener(
},
};
}
async function assertPostInteractionNavigationSafe(opts: {
cdpUrl: string;
page: Awaited<ReturnType<typeof getPageForTargetId>>;
ssrfPolicy?: SsrFPolicy;
targetId?: string;
}): Promise<void> {
if (!opts.ssrfPolicy) {
return;
}
await assertPageNavigationCompletedSafely({
cdpUrl: opts.cdpUrl,
page: opts.page,
response: null,
ssrfPolicy: opts.ssrfPolicy,
targetId: opts.targetId,
});
}
export async function highlightViaPlaywright(opts: {
cdpUrl: string;
targetId?: string;
@@ -559,12 +540,16 @@ export async function pressKeyViaPlaywright(opts: {
}
const page = await getPageForTargetId(opts);
ensurePageState(page);
await page.keyboard.press(key, {
delay: Math.max(0, Math.floor(opts.delayMs ?? 0)),
});
await assertPostInteractionNavigationSafe({
const previousUrl = page.url();
await assertInteractionNavigationCompletedSafely({
action: async () => {
await page.keyboard.press(key, {
delay: Math.max(0, Math.floor(opts.delayMs ?? 0)),
});
},
cdpUrl: opts.cdpUrl,
page,
previousUrl,
ssrfPolicy: opts.ssrfPolicy,
targetId: opts.targetId,
});
@@ -597,10 +582,14 @@ export async function typeViaPlaywright(opts: {
await locator.fill(text, { timeout });
}
if (opts.submit) {
await locator.press("Enter", { timeout });
await assertPostInteractionNavigationSafe({
const previousUrl = page.url();
await assertInteractionNavigationCompletedSafely({
action: async () => {
await locator.press("Enter", { timeout });
},
cdpUrl: opts.cdpUrl,
page,
previousUrl,
ssrfPolicy: opts.ssrfPolicy,
targetId: opts.targetId,
});