From 566f5f09a7a88d5e904ed124f3913a26a9cfc71b Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sat, 25 Apr 2026 23:57:50 -0700 Subject: [PATCH 1/2] fix(cua-driver): address CodeRabbit review comments on PR 1387 - BrowserJS.cgWindowTitle: return "" (not nil) when window found but has empty title (new tabs, about:blank) to avoid false windowNotFound errors. AppleScript `name contains ""` matches any window and falls through to the front window fallback correctly. - PageTool.query_dom: use jsonString() helper to escape attribute names before injecting into the generated JS array literal, preventing XSS via malformed attribute names. Co-Authored-By: Claude Sonnet 4.6 --- libs/cua-driver/Sources/CuaDriverServer/Tools/PageTool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cua-driver/Sources/CuaDriverServer/Tools/PageTool.swift b/libs/cua-driver/Sources/CuaDriverServer/Tools/PageTool.swift index e9a737fcb1..1fc8a6cfc1 100644 --- a/libs/cua-driver/Sources/CuaDriverServer/Tools/PageTool.swift +++ b/libs/cua-driver/Sources/CuaDriverServer/Tools/PageTool.swift @@ -148,7 +148,7 @@ public enum PageTool { } let attrJS = attrs.isEmpty ? "[]" - : "[\(attrs.map { "\"\($0)\"" }.joined(separator: ", "))]" + : "[\(attrs.map { jsonString($0) }.joined(separator: ", "))]" let js = """ (() => { const attrs = \(attrJS); From dbcc536886acad59f77acb26a8947e10d6bb06d6 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sun, 26 Apr 2026 00:39:04 -0700 Subject: [PATCH 2/2] fix(cua-driver): validate CDP port ownership by pid before probing in ElectronJS pageTarget() was probing fixed ports (9222-9225, 9230) and returning the first "page" target found, regardless of which process owned the port. If another Electron/Chromium app was already listening on port 9222, JS would execute in the wrong app. Use lsof to get ports actually owned by the target pid and limit probing to those. Falls back to all candidate ports when lsof returns empty (race window before SIGUSR1 activates the inspector). Co-Authored-By: Claude Sonnet 4.6 --- .../CuaDriverCore/Browser/ElectronJS.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/libs/cua-driver/Sources/CuaDriverCore/Browser/ElectronJS.swift b/libs/cua-driver/Sources/CuaDriverCore/Browser/ElectronJS.swift index 9657196a33..5d1e79bbe1 100644 --- a/libs/cua-driver/Sources/CuaDriverCore/Browser/ElectronJS.swift +++ b/libs/cua-driver/Sources/CuaDriverCore/Browser/ElectronJS.swift @@ -77,7 +77,7 @@ public enum ElectronJS { // Prefer a "page" target (renderer with DOM) if the app was launched with // --remote-debugging-port. Page targets expose document/window/DOM APIs. - if let pagePort = await pageTarget() { + if let pagePort = await pageTarget(pid: pid) { return try await cdpEvaluate(port: pagePort, javascript: javascript) } @@ -130,8 +130,20 @@ public enum ElectronJS { /// DOM access). Returns the port if found. Apps launched with /// `--remote-debugging-port=N` expose page targets here; apps activated via /// SIGUSR1 only expose a "node" main-process target with no DOM. - private static func pageTarget() async -> Int? { - for port in [9222, 9223, 9224, 9225, 9230] { + /// + /// Only probes ports that `lsof` confirms are owned by `pid`, so JS is never + /// executed in a different Electron/Chromium app that happens to be listening + /// on the same well-known port. Falls back to all candidate ports when + /// `lsof` returns empty (race window before the inspector has started). + private static func pageTarget(pid: Int32) async -> Int? { + let candidatePorts = [9222, 9223, 9224, 9225, 9230] + let ownedPorts = await listeningPorts(pid: pid) + // Only probe ports owned by this pid; fall back to all candidates when + // lsof returns empty (inspector hasn't started yet). + let portsToProbe = ownedPorts.isEmpty + ? candidatePorts + : candidatePorts.filter { ownedPorts.contains($0) } + for port in portsToProbe { guard let url = URL(string: "http://127.0.0.1:\(port)/json") else { continue } var req = URLRequest(url: url); req.timeoutInterval = 0.3 guard let (data, _) = try? await URLSession.shared.data(for: req),