Skip to content

Commit 7027d2c

Browse files
Leon-SK668steipete
andauthored
fix(browser): reject non-page json new targets (#104129)
* fix(browser): reject non-page json new targets * fix(browser): adopt only validated raw CDP targets --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 3f7c783 commit 7027d2c

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,44 @@ describe("browser remote profile fallback and attachOnly behavior", () => {
181181
await expect(remote.openTab("about:blank")).rejects.toBeInstanceOf(
182182
deps.BrowserCdpEndpointBlockedError,
183183
);
184+
expect(state.profiles.get("remote")?.lastTargetId).not.toBe("T_BLOCKED");
185+
});
186+
187+
it.each([
188+
{
189+
id: "WORKER",
190+
title: "Worker",
191+
url: "https://example.com/worker.js",
192+
type: "worker",
193+
},
194+
{
195+
id: "INTERNAL",
196+
title: "Settings",
197+
url: "chrome://settings/",
198+
type: "page",
199+
},
200+
])("rejects non-selectable $type target $id returned by raw tab creation", async (created) => {
201+
vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue(null);
202+
vi.spyOn(deps.cdpModule, "createTargetViaCdp").mockRejectedValue(
203+
new Error("Target.createTarget unavailable"),
204+
);
205+
const fetchMock = vi.fn(async (url: unknown) => {
206+
const u = String(url);
207+
if (!u.includes("/json/new")) {
208+
throw new Error(`unexpected fetch: ${u}`);
209+
}
210+
return {
211+
ok: true,
212+
json: async () => ({
213+
...created,
214+
webSocketDebuggerUrl: `wss://1.1.1.1:9222/devtools/page/${created.id}`,
215+
}),
216+
} as unknown as Response;
217+
});
218+
const { state, remote } = deps.createRemoteRouteHarness(fetchMock);
219+
220+
await expect(remote.openTab("https://example.com")).rejects.toThrow(/non-selectable target/);
221+
expect(state.profiles.get("remote")?.lastTargetId).not.toBe(created.id);
184222
});
185223

186224
it("fails closed for remote tab opens in strict mode without Playwright", async () => {

extensions/browser/src/browser/server-context.tab-ops.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,10 @@ export function createProfileTabOps({ profile, state, runtime }: TabOpsDeps): Pr
442442
if (!created.id) {
443443
throw new Error("Failed to open tab (missing id)");
444444
}
445-
runtime.lastTargetId = created.id;
446445
const resolvedUrl = created.url ?? url;
446+
if (!isSelectableCdpBrowserTarget({ url: resolvedUrl, type: created.type })) {
447+
throw new Error("Failed to open tab (non-selectable target)");
448+
}
447449
await assertBrowserNavigationResultAllowed({ url: resolvedUrl, ...ssrfPolicyOpts });
448450
const wsUrl = normalizeWsUrl(created.webSocketDebuggerUrl, profile.cdpUrl);
449451
if (wsUrl) {
@@ -452,6 +454,9 @@ export function createProfileTabOps({ profile, state, runtime }: TabOpsDeps): Pr
452454
configuredUrl: profile.cdpUrl,
453455
});
454456
}
457+
// Adopt only fully validated targets. A failed open must not make a
458+
// blocked or non-page target sticky for the next implicit action.
459+
runtime.lastTargetId = created.id;
455460
triggerManagedTabLimit(created.id, opts);
456461
return assignTabAlias({
457462
profileState: runtime,

0 commit comments

Comments
 (0)