mirror of
https://github.com/home-assistant/iOS.git
synced 2026-04-17 08:05:44 -05:00
Prevent execution of WKUserScript from beyond main frame and allowed hosts (#4469)
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
This commit is contained in:
committed by
GitHub
parent
a20ae2f593
commit
c8f35e4cff
@@ -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 */; };
|
||||
@@ -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>"; };
|
||||
@@ -4481,6 +4483,7 @@
|
||||
42A47A862C452D5400C9B43D /* WebViewExternalMessageHandlerTests.swift */,
|
||||
429481EA2DA93FA000A8B468 /* WebViewJavascriptCommandsTests.swift */,
|
||||
4228D0002DB903AA00FC6912 /* WKUserContentControllerMessageTests.swift */,
|
||||
42C5E5AC2F7E74DE004797B5 /* SafeScriptMessageHandlerTests.swift */,
|
||||
);
|
||||
path = WebView;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -204,7 +204,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
|
||||
userContentController.addUserScript(WKUserScript(
|
||||
source: wsBridgeJS,
|
||||
injectionTime: .atDocumentEnd,
|
||||
forMainFrameOnly: false
|
||||
forMainFrameOnly: true
|
||||
))
|
||||
|
||||
userContentController.addUserScript(.init(
|
||||
@@ -219,7 +219,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
|
||||
});
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: false
|
||||
forMainFrameOnly: true
|
||||
))
|
||||
|
||||
config.userContentController = userContentController
|
||||
|
||||
42
Tests/App/WebView/SafeScriptMessageHandlerTests.swift
Normal file
42
Tests/App/WebView/SafeScriptMessageHandlerTests.swift
Normal file
@@ -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) {}
|
||||
}
|
||||
Reference in New Issue
Block a user