fix(macos): show local ClickClack session discussions (#111883)

* fix(macos): show local ClickClack session discussions

* fix(macos): support loopback aliases in discussion embeds

* fix(macos): allow trusted hosted ClickClack embeds

* fix(macos): trust only dashboard-initiated discussion frames
This commit is contained in:
Peter Steinberger
2026-07-20 09:33:27 -07:00
committed by GitHub
parent ec9bc46e80
commit 38df399ed8
2 changed files with 75 additions and 8 deletions
@@ -903,15 +903,35 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
return String(raw.dropFirst().dropLast())
}
static func shouldAllowNavigation(to url: URL, dashboardURL: URL) -> Bool {
static func shouldAllowNavigation(
to url: URL,
dashboardURL: URL,
isMainFrame: Bool,
isTrustedDashboardSource: Bool = false) -> Bool
{
guard let scheme = url.scheme?.lowercased() else { return true }
if scheme == "about" || scheme == "blob" || scheme == "data" {
return true
}
guard scheme == "http" || scheme == "https" else { return false }
return url.scheme?.lowercased() == dashboardURL.scheme?.lowercased() &&
url.host?.lowercased() == dashboardURL.host?.lowercased() &&
url.port == dashboardURL.port
let dashboardScheme = dashboardURL.scheme?.lowercased()
let dashboardHost = dashboardURL.host?.lowercased()
let host = url.host?.lowercased()
if scheme == dashboardScheme, host == dashboardHost, url.port == dashboardURL.port {
return true
}
guard !isMainFrame,
isTrustedDashboardSource,
host?.isEmpty == false,
url.user == nil,
url.password == nil
else {
return false
}
let components = url.path.split(separator: "/", omittingEmptySubsequences: true)
return components.count == 4 &&
components[0] == "embed" &&
(components[1] == "channel" || components[1] == "thread")
}
static func shouldAllowBrowserNavigation(to url: URL, isMainFrame: Bool) -> Bool {
@@ -1118,7 +1138,15 @@ extension DashboardWindowController {
decisionHandler: decisionHandler)
return
}
if Self.shouldAllowNavigation(to: url, dashboardURL: self.currentURL) {
if Self.shouldAllowNavigation(
to: url,
dashboardURL: self.currentURL,
isMainFrame: navigationAction.targetFrame?.isMainFrame == true,
isTrustedDashboardSource: navigationAction.sourceFrame.isMainFrame &&
Self.isTrustedLinkSource(
navigationAction.sourceFrame.request.url,
dashboardURL: self.currentURL))
{
decisionHandler(.allow)
return
}
@@ -188,19 +188,58 @@ struct DashboardWindowSmokeTests {
let staleEndpoint = try #require(URL(string: "http://127.0.0.1:18790/control/chat"))
#expect(try DashboardWindowController.shouldAllowNavigation(
to: #require(URL(string: "http://127.0.0.1:18789/control/chat")),
dashboardURL: dashboard))
dashboardURL: dashboard,
isMainFrame: true))
#expect(try !DashboardWindowController.shouldAllowNavigation(
to: #require(URL(string: "https://docs.openclaw.ai/")),
dashboardURL: dashboard))
dashboardURL: dashboard,
isMainFrame: true))
#expect(!DashboardWindowController.shouldAllowNavigation(
to: staleEndpoint,
dashboardURL: dashboard))
dashboardURL: dashboard,
isMainFrame: true))
#expect(!DashboardWindowController.shouldOpenExternalDashboardNavigation(
staleEndpoint,
navigationType: .backForward,
buttonNumber: 1))
}
@Test func `dashboard permits only trusted ClickClack discussion subframes`() throws {
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
let channel = try #require(URL(string: "http://127.0.0.1:18890/embed/channel/T01/C01"))
let thread = try #require(URL(string: "http://127.0.0.1:18890/embed/thread/T01/M01"))
let hostnameAlias = try #require(URL(string: "http://localhost:18890/embed/channel/T01/C01"))
let ipv6Alias = try #require(URL(string: "http://[::1]:18890/embed/thread/T01/M01"))
let credentialedFrame = try #require(URL(string: "http://user:pass@localhost:18890/embed/channel/T01/C01"))
let unrelatedPath = try #require(URL(string: "http://127.0.0.1:18890/admin"))
let externalFrame = try #require(URL(string: "https://clickclack.example/embed/channel/T01/C01"))
let externalHTTPFrame = try #require(URL(string: "http://clickclack.example/embed/thread/T01/M01"))
let localFile = try #require(URL(string: "file:///tmp/discussion.html"))
#expect(DashboardWindowController.shouldAllowNavigation(
to: channel, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(DashboardWindowController.shouldAllowNavigation(
to: thread, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(DashboardWindowController.shouldAllowNavigation(
to: hostnameAlias, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(DashboardWindowController.shouldAllowNavigation(
to: ipv6Alias, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(DashboardWindowController.shouldAllowNavigation(
to: externalFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(DashboardWindowController.shouldAllowNavigation(
to: externalHTTPFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(!DashboardWindowController.shouldAllowNavigation(
to: channel, dashboardURL: dashboard, isMainFrame: true))
#expect(!DashboardWindowController.shouldAllowNavigation(
to: credentialedFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(!DashboardWindowController.shouldAllowNavigation(
to: unrelatedPath, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
#expect(!DashboardWindowController.shouldAllowNavigation(
to: externalFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: false))
#expect(!DashboardWindowController.shouldAllowNavigation(
to: localFile, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
}
@Test func `dashboard navigation shortcuts target the focused browser`() throws {
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
let controller = DashboardWindowController(