Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@
42C131D02D66084C00AF48E6 /* PillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C131CF2D66084C00AF48E6 /* PillView.swift */; };
42C3737F2BC415AC00898990 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3737E2BC415AC00898990 /* UIViewController+Extensions.swift */; };
42C5E5AB2F7C20EA004797B5 /* EntityColorAttributesParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42145A652F7F4CC000891E04 /* EntityColorAttributesParser.swift */; };
42C5E5AD2F7E74DE004797B5 /* SafeScriptMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C5E5AC2F7E74DE004797B5 /* SafeScriptMessageHandlerTests.swift */; };
42C60FA62F081DA90071A6F6 /* DeviceClassProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C60FA52F081DA90071A6F6 /* DeviceClassProvider.swift */; };
42C60FA72F081DA90071A6F6 /* DeviceClassProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C60FA52F081DA90071A6F6 /* DeviceClassProvider.swift */; };
42CB330D2DAE4FD800491DCE /* ServerSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CB330C2DAE4FD800491DCE /* ServerSelectView.swift */; };
Expand Down Expand Up @@ -2877,6 +2878,7 @@
42C131CF2D66084C00AF48E6 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
42C3737E2BC415AC00898990 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = "<group>"; };
42C373AF2BC536AA00898990 /* WatchApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WatchApp-Bridging-Header.h"; sourceTree = "<group>"; };
42C5E5AC2F7E74DE004797B5 /* SafeScriptMessageHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeScriptMessageHandlerTests.swift; sourceTree = "<group>"; };
42C60FA52F081DA90071A6F6 /* DeviceClassProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceClassProvider.swift; sourceTree = "<group>"; };
42CA28AD2B101D4D0093B31A /* HACornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HACornerRadius.swift; sourceTree = "<group>"; };
42CA28AF2B101D6B0093B31A /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4481,6 +4483,7 @@
42A47A862C452D5400C9B43D /* WebViewExternalMessageHandlerTests.swift */,
429481EA2DA93FA000A8B468 /* WebViewJavascriptCommandsTests.swift */,
4228D0002DB903AA00FC6912 /* WKUserContentControllerMessageTests.swift */,
42C5E5AC2F7E74DE004797B5 /* SafeScriptMessageHandlerTests.swift */,
);
path = WebView;
sourceTree = "<group>";
Expand Down Expand Up @@ -9617,6 +9620,7 @@
422E626C2CDCF00A00987BD0 /* AreasService.test.swift in Sources */,
11A71C8D24A593A800D9565F /* ZoneManagerCollector.test.swift in Sources */,
42B980DC2DC256A300BC5C08 /* SensorRow.test.swift in Sources */,
42C5E5AD2F7E74DE004797B5 /* SafeScriptMessageHandlerTests.swift in Sources */,
116D3A3D2724D83300EF5D21 /* OnboardingAuth.test.swift in Sources */,
BB77559927344584B2C0E987 /* OnboardingAuthError.test.swift in Sources */,
420F53EE2C4EA025003C8415 /* WidgetsKindTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import Foundation
import Shared
import WebKit

/// Use to avoid holding webview alive when adding WKScriptMessageHandler
final class SafeScriptMessageHandler: NSObject, WKScriptMessageHandler {
let server: Server
weak var delegate: WKScriptMessageHandler?
init(delegate: WKScriptMessageHandler) {
init(server: Server, delegate: WKScriptMessageHandler) {
self.server = server
self.delegate = delegate
super.init()
}
Expand All @@ -13,8 +16,29 @@ final class SafeScriptMessageHandler: NSObject, WKScriptMessageHandler {
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
// Only the top-level document on an allowed server host may talk to the native bridge.
guard shouldAllowMessage(
isMainFrame: message.frameInfo.isMainFrame,
host: message.frameInfo.securityOrigin.host
) else {
return
}
delegate?.userContentController(
userContentController, didReceive: message
)
}

func shouldAllowMessage(isMainFrame: Bool, host: String) -> Bool {
isMainFrame && allowedHosts.contains(host)
}

private var allowedHosts: Set<String> {
let urls = [
server.info.connection.address(for: .internal),
server.info.connection.address(for: .external),
server.info.connection.address(for: .remoteUI),
]

return Set(urls.compactMap { $0?.host })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UIKit
extension WebViewController {
func setupUserContentController() -> WKUserContentController {
let userContentController = WKUserContentController()
let safeScriptMessageHandler = SafeScriptMessageHandler(delegate: webViewScriptMessageHandler)
let safeScriptMessageHandler = SafeScriptMessageHandler(server: server, delegate: webViewScriptMessageHandler)
userContentController.add(safeScriptMessageHandler, name: "getExternalAuth")
userContentController.add(safeScriptMessageHandler, name: "revokeExternalAuth")
userContentController.add(safeScriptMessageHandler, name: "externalBus")
Expand Down
4 changes: 2 additions & 2 deletions Sources/App/Frontend/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
userContentController.addUserScript(WKUserScript(
source: wsBridgeJS,
injectionTime: .atDocumentEnd,
forMainFrameOnly: false
forMainFrameOnly: true
))

userContentController.addUserScript(.init(
Expand All @@ -219,7 +219,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
});
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: false
forMainFrameOnly: true
))

config.userContentController = userContentController
Expand Down
42 changes: 42 additions & 0 deletions Tests/App/WebView/SafeScriptMessageHandlerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@testable import HomeAssistant
import Shared
import Testing
import WebKit

struct SafeScriptMessageHandlerTests {
@Test func allowsMainFrameMessageFromConfiguredServerHost() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)

#expect(handler.shouldAllowMessage(isMainFrame: true, host: "external.example.com"))
#expect(handler.shouldAllowMessage(isMainFrame: true, host: "internal.example.com"))
#expect(handler.shouldAllowMessage(isMainFrame: true, host: "ui.nabu.casa"))
}

@Test func rejectsMessageFromHostOutsideConfiguredServerHosts() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)

#expect(!handler.shouldAllowMessage(isMainFrame: true, host: "evil.example.com"))
}

@Test func rejectsIframeMessageEvenWhenHostIsAllowed() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)

#expect(!handler.shouldAllowMessage(isMainFrame: false, host: "external.example.com"))
}
}

private final class NoOpScriptMessageHandler: NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {}
}
Loading